{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|default_exp data.preprocessing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Data preprocessing"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">Functions used to preprocess time series (both X and y)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "from __future__ import annotations\n",
    "from tsai.imports import *\n",
    "import re\n",
    "from joblib import dump, load\n",
    "import sklearn\n",
    "from sklearn.base import BaseEstimator, TransformerMixin\n",
    "from pandas._libs.tslibs.timestamps import Timestamp\n",
    "from fastcore.transform import Transform, ItemTransform, Pipeline\n",
    "from fastai.data.transforms import Categorize\n",
    "from fastai.data.load import DataLoader\n",
    "from fastai.tabular.core import df_shrink_dtypes, make_date\n",
    "from tsai.utils import *\n",
    "from tsai.data.core import *\n",
    "from tsai.data.preparation import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tsai.data.external import get_UCR_data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dsid = 'NATOPS'\n",
    "X, y, splits = get_UCR_data(dsid, return_split=False)\n",
    "tfms = [None, Categorize()]\n",
    "dsets = TSDatasets(X, y, tfms=tfms, splits=splits)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class ToNumpyCategory(Transform):\n",
    "    \"Categorize a numpy batch\"\n",
    "    order = 90\n",
    "\n",
    "    def __init__(self, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: np.ndarray):\n",
    "        self.type = type(o)\n",
    "        self.cat = Categorize()\n",
    "        self.cat.setup(o)\n",
    "        self.vocab = self.cat.vocab\n",
    "        return np.asarray(stack([self.cat(oi) for oi in o]))\n",
    "\n",
    "    def decodes(self, o: np.ndarray):\n",
    "        return stack([self.cat.decode(oi) for oi in o])\n",
    "\n",
    "    def decodes(self, o: torch.Tensor):\n",
    "        return stack([self.cat.decode(oi) for oi in o])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([3, 2, 2, 3, 2, 4, 0, 5, 2, 1])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = ToNumpyCategory()\n",
    "y_cat = t(y)\n",
    "y_cat[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_eq(t.decode(tensor(y_cat)), y)\n",
    "test_eq(t.decode(np.array(y_cat)), y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class OneHot(Transform): \n",
    "    \"One-hot encode/ decode a batch\"\n",
    "    order = 90\n",
    "    def __init__(self, n_classes=None, **kwargs): \n",
    "        self.n_classes = n_classes\n",
    "        super().__init__(**kwargs)\n",
    "    def encodes(self, o: torch.Tensor): \n",
    "        if not self.n_classes: self.n_classes = len(np.unique(o))\n",
    "        return torch.eye(self.n_classes)[o]\n",
    "    def encodes(self, o: np.ndarray): \n",
    "        o = ToNumpyCategory()(o)\n",
    "        if not self.n_classes: self.n_classes = len(np.unique(o))\n",
    "        return np.eye(self.n_classes)[o]\n",
    "    def decodes(self, o: torch.Tensor): return torch.argmax(o, dim=-1)\n",
    "    def decodes(self, o: np.ndarray): return np.argmax(o, axis=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[0., 0., 0., 1., 0., 0.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 0., 1., 0., 0.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 1., 0.],\n",
       "       [1., 0., 0., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 1.],\n",
       "       [0., 0., 1., 0., 0., 0.],\n",
       "       [0., 1., 0., 0., 0., 0.]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "oh_encoder = OneHot()\n",
    "y_cat = ToNumpyCategory()(y)\n",
    "oht = oh_encoder(y_cat)\n",
    "oht[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_classes = 10\n",
    "n_samples = 100\n",
    "\n",
    "t = torch.randint(0, n_classes, (n_samples,))\n",
    "oh_encoder = OneHot()\n",
    "oht = oh_encoder(t)\n",
    "test_eq(oht.shape, (n_samples, n_classes))\n",
    "test_eq(torch.argmax(oht, dim=-1), t)\n",
    "test_eq(oh_encoder.decode(oht), t)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_classes = 10\n",
    "n_samples = 100\n",
    "\n",
    "a = np.random.randint(0, n_classes, (n_samples,))\n",
    "oh_encoder = OneHot()\n",
    "oha = oh_encoder(a)\n",
    "test_eq(oha.shape, (n_samples, n_classes))\n",
    "test_eq(np.argmax(oha, axis=-1), a)\n",
    "test_eq(oh_encoder.decode(oha), a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSNan2Value(Transform):\n",
    "    \"Replaces any nan values by a predefined value or median\"\n",
    "    order = 90\n",
    "    def __init__(self, value=0, median=False, by_sample_and_var=True, sel_vars=None):\n",
    "        store_attr()\n",
    "        if not ismin_torch(\"1.8\"):\n",
    "            raise ValueError('This function only works with Pytorch>=1.8.')\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.sel_vars is not None: \n",
    "            mask = torch.isnan(o[:, self.sel_vars])\n",
    "            if mask.any() and self.median:\n",
    "                if self.by_sample_and_var:\n",
    "                    median = torch.nanmedian(o[:, self.sel_vars], dim=2, keepdim=True)[0].repeat(1, 1, o.shape[-1])\n",
    "                    o[:, self.sel_vars][mask] = median[mask]\n",
    "                else:\n",
    "                    o[:, self.sel_vars] = torch.nan_to_num(o[:, self.sel_vars], torch.nanmedian(o[:, self.sel_vars]))\n",
    "            o[:, self.sel_vars] = torch.nan_to_num(o[:, self.sel_vars], self.value)\n",
    "        else:\n",
    "            mask = torch.isnan(o)\n",
    "            if mask.any() and self.median:\n",
    "                if self.by_sample_and_var:\n",
    "                    median = torch.nanmedian(o, dim=2, keepdim=True)[0].repeat(1, 1, o.shape[-1])\n",
    "                    o[mask] = median[mask]\n",
    "                else:\n",
    "                    o = torch.nan_to_num(o, torch.nanmedian(o))\n",
    "            o = torch.nan_to_num(o, self.value)\n",
    "        return o\n",
    "\n",
    "\n",
    "    \n",
    "Nan2Value = TSNan2Value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "o = TSTensor(torch.randn(16, 10, 100))\n",
    "o[0,0] = float('nan')\n",
    "o[o > .9] = float('nan')\n",
    "o[[0,1,5,8,14,15], :, -20:] = float('nan')\n",
    "nan_vals1 = torch.isnan(o).sum()\n",
    "o2 = Pipeline(TSNan2Value(), split_idx=0)(o.clone())\n",
    "o3 = Pipeline(TSNan2Value(median=True, by_sample_and_var=True), split_idx=0)(o.clone())\n",
    "o4 = Pipeline(TSNan2Value(median=True, by_sample_and_var=False), split_idx=0)(o.clone())\n",
    "nan_vals2 = torch.isnan(o2).sum()\n",
    "nan_vals3 = torch.isnan(o3).sum()\n",
    "nan_vals4 = torch.isnan(o4).sum()\n",
    "test_ne(nan_vals1, 0)\n",
    "test_eq(nan_vals2, 0)\n",
    "test_eq(nan_vals3, 0)\n",
    "test_eq(nan_vals4, 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "o = TSTensor(torch.randn(16, 10, 100))\n",
    "o[o > .9] = float('nan')\n",
    "o = TSNan2Value(median=True, sel_vars=[0,1,2,3,4])(o)\n",
    "test_eq(torch.isnan(o[:, [0,1,2,3,4]]).sum().item(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSStandardize(Transform):\n",
    "    \"\"\"Standardizes batch of type `TSTensor`\n",
    "\n",
    "    Args:\n",
    "        - mean: you can pass a precalculated mean value as a torch tensor which is the one that will be used, or leave as None, in which case\n",
    "            it will be estimated using a batch.\n",
    "        - std: you can pass a precalculated std value as a torch tensor which is the one that will be used, or leave as None, in which case\n",
    "            it will be estimated using a batch. If both mean and std values are passed when instantiating TSStandardize, the rest of arguments won't be used.\n",
    "        - by_sample: if True, it will calculate mean and std for each individual sample. Otherwise based on the entire batch.\n",
    "        - by_var:\n",
    "            * False: mean and std will be the same for all variables.\n",
    "            * True: a mean and std will be be different for each variable.\n",
    "            * a list of ints: (like [0,1,3]) a different mean and std will be set for each variable on the list. Variables not included in the list\n",
    "            won't be standardized.\n",
    "            * a list that contains a list/lists: (like[0, [1,3]]) a different mean and std will be set for each element of the list. If multiple elements are\n",
    "            included in a list, the same mean and std will be set for those variable in the sublist/s. (in the example a mean and std is determined for\n",
    "            variable 0, and another one for variables 1 & 3 - the same one). Variables not included in the list won't be standardized.\n",
    "        - by_step: if False, it will standardize values for each time step.\n",
    "        - exc_vars: list of variables that won't be standardized.\n",
    "        - eps: it avoids dividing by 0\n",
    "        - use_single_batch: if True a single training batch will be used to calculate mean & std. Else the entire training set will be used.\n",
    "    \"\"\"\n",
    "\n",
    "    parameters, order = L('mean', 'std'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, mean=None, std=None, by_sample=False, by_var=False, by_step=False, exc_vars=None, eps=1e-8, use_single_batch=True, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.mean = tensor(mean) if mean is not None else None\n",
    "        self.std = tensor(std) if std is not None else None\n",
    "        self._setup = (mean is None or std is None) and not by_sample\n",
    "        self.eps = eps\n",
    "        self.by_sample, self.by_var, self.by_step = by_sample, by_var, by_step\n",
    "        drop_axes = []\n",
    "        if by_sample: drop_axes.append(0)\n",
    "        if by_var: drop_axes.append(1)\n",
    "        if by_step: drop_axes.append(2)\n",
    "        self.exc_vars = exc_vars\n",
    "        self.axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes])\n",
    "        if by_var and is_listy(by_var):\n",
    "            self.list_axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes]) + (1,)\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.verbose = verbose\n",
    "        if self.mean is not None or self.std is not None:\n",
    "            pv(f'{self.__class__.__name__} mean={self.mean}, std={self.std}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n', \n",
    "               self.verbose)\n",
    "\n",
    "    @classmethod\n",
    "    def from_stats(cls, mean, std): return cls(mean, std)\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                mean = torch.zeros(*shape, device=o.device)\n",
    "                std = torch.ones(*shape, device=o.device)\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    mean[:, v] = torch_nanmean(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True)\n",
    "                    std[:, v] = torch.clamp_min(torch_nanstd(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True), self.eps)\n",
    "            else:\n",
    "                mean = torch_nanmean(o, dim=self.axes, keepdim=self.axes!=())\n",
    "                std = torch.clamp_min(torch_nanstd(o, dim=self.axes, keepdim=self.axes!=()), self.eps)\n",
    "            if self.exc_vars is not None:\n",
    "                mean[:, self.exc_vars] = 0.\n",
    "                std[:, self.exc_vars] = 1.\n",
    "            self.mean, self.std = mean, std\n",
    "            if len(self.mean.shape) == 0:\n",
    "                pv(f'{self.__class__.__name__} mean={self.mean}, std={self.std}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "                   self.verbose)\n",
    "            else:\n",
    "                pv(f'{self.__class__.__name__} mean shape={self.mean.shape}, std shape={self.std.shape}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n',\n",
    "                   self.verbose)\n",
    "            self._setup = False\n",
    "        elif self.by_sample: self.mean, self.std = torch.zeros(1), torch.ones(1)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.by_sample:\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                mean = torch.zeros(*shape, device=o.device)\n",
    "                std = torch.ones(*shape, device=o.device)\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    mean[:, v] = torch_nanmean(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True)\n",
    "                    std[:, v] = torch.clamp_min(torch_nanstd(o[:, v], dim=self.axes if len(v) == 1 else self.list_axes, keepdim=True), self.eps)\n",
    "            else:\n",
    "                mean = torch_nanmean(o, dim=self.axes, keepdim=self.axes!=())\n",
    "                std = torch.clamp_min(torch_nanstd(o, dim=self.axes, keepdim=self.axes!=()), self.eps)\n",
    "            if self.exc_vars is not None:\n",
    "                mean[:, self.exc_vars] = 0.\n",
    "                std[:, self.exc_vars] = 1.\n",
    "            self.mean, self.std = mean, std\n",
    "        return (o - self.mean) / self.std\n",
    "\n",
    "    def decodes(self, o:TSTensor):\n",
    "        if self.mean is None or self.std is None: return o\n",
    "        return o * self.std + self.mean\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms=[TSStandardize(by_sample=True, by_var=False, verbose=True)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, batch_tfms=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([ 0.0000, -1.3398,  0.0000,  0.9952, -0.8438, -0.4308,  0.0000, -0.6077,\n",
      "         0.0000,  0.7781, -0.4869, -0.0969,  0.0000, -1.0620, -0.6171,  0.9253,\n",
      "        -0.7023, -0.3077, -0.5600, -1.1922, -0.7503,  0.9491, -0.7744, -0.4356])\n",
      "tensor([1.0000, 0.8743, 1.0000, 0.7510, 1.1557, 0.5370, 1.0000, 0.2666, 1.0000,\n",
      "        0.2380, 0.4047, 0.3274, 1.0000, 0.6371, 0.2798, 0.5287, 0.8642, 0.4297,\n",
      "        0.5842, 0.7581, 0.3162, 0.6739, 1.0118, 0.4958])\n"
     ]
    }
   ],
   "source": [
    "exc_vars = [0, 2, 6, 8, 12]\n",
    "batch_tfms=[TSStandardize(by_var=True, exc_vars=exc_vars)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, batch_tfms=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_eq(len(dls.train.after_batch.fs[0].mean.flatten()), 24)\n",
    "test_eq(len(dls.train.after_batch.fs[0].std.flatten()), 24)\n",
    "test_eq(dls.train.after_batch.fs[0].mean.flatten()[exc_vars].cpu(), torch.zeros(len(exc_vars)))\n",
    "test_eq(dls.train.after_batch.fs[0].std.flatten()[exc_vars].cpu(), torch.ones(len(exc_vars)))\n",
    "print(dls.train.after_batch.fs[0].mean.flatten().data)\n",
    "print(dls.train.after_batch.fs[0].std.flatten().data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tsai.data.validation import TimeSplitter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_nan = np.random.rand(100, 5, 10)\n",
    "idxs = random_choice(len(X_nan), int(len(X_nan)*.5), False)\n",
    "X_nan[idxs, 0] = float('nan')\n",
    "idxs = random_choice(len(X_nan), int(len(X_nan)*.5), False)\n",
    "X_nan[idxs, 1, -10:] = float('nan')\n",
    "batch_tfms = TSStandardize(by_var=True)\n",
    "dls = get_ts_dls(X_nan, batch_tfms=batch_tfms, splits=TimeSplitter(show_plot=False)(range_of(X_nan)))\n",
    "test_eq(torch.isnan(dls.after_batch[0].mean).sum(), 0)\n",
    "test_eq(torch.isnan(dls.after_batch[0].std).sum(), 0)\n",
    "xb = first(dls.train)[0]\n",
    "test_ne(torch.isnan(xb).sum(), 0)\n",
    "test_ne(torch.isnan(xb).sum(), torch.isnan(xb).numel())\n",
    "batch_tfms = [TSStandardize(by_var=True), Nan2Value()]\n",
    "dls = get_ts_dls(X_nan, batch_tfms=batch_tfms, splits=TimeSplitter(show_plot=False)(range_of(X_nan)))\n",
    "xb = first(dls.train)[0]\n",
    "test_eq(torch.isnan(xb).sum(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms=[TSStandardize(by_sample=True, by_var=False, verbose=False)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)\n",
    "xb, yb = next(iter(dls.valid))\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfms = [None, TSClassification()]\n",
    "batch_tfms = TSStandardize(by_sample=True)\n",
    "dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=[64, 128], inplace=True)\n",
    "xb, yb = dls.train.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)\n",
    "xb, yb = dls.valid.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tfms = [None, TSClassification()]\n",
    "batch_tfms = TSStandardize(by_sample=True, by_var=False, verbose=False)\n",
    "dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=[64, 128], inplace=False)\n",
    "xb, yb = dls.train.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)\n",
    "xb, yb = dls.valid.one_batch()\n",
    "test_close(xb.mean(), 0, eps=1e-1)\n",
    "test_close(xb.std(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "\n",
    "@patch\n",
    "def mul_min(x:torch.Tensor|TSTensor|NumpyTensor, axes=(), keepdim=False):\n",
    "    if axes == (): return retain_type(x.min(), x)\n",
    "    axes = reversed(sorted(axes if is_listy(axes) else [axes]))\n",
    "    min_x = x\n",
    "    for ax in axes: min_x, _ = min_x.min(ax, keepdim)\n",
    "    return retain_type(min_x, x)\n",
    "\n",
    "\n",
    "@patch\n",
    "def mul_max(x:torch.Tensor|TSTensor|NumpyTensor, axes=(), keepdim=False):\n",
    "    if axes == (): return retain_type(x.max(), x)\n",
    "    axes = reversed(sorted(axes if is_listy(axes) else [axes]))\n",
    "    max_x = x\n",
    "    for ax in axes: max_x, _ = max_x.max(ax, keepdim)\n",
    "    return retain_type(max_x, x)\n",
    "\n",
    "\n",
    "class TSNormalize(Transform):\n",
    "    \"Normalizes batch of type `TSTensor`\"\n",
    "    parameters, order = L('min', 'max'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, min=None, max=None, range=(-1, 1), by_sample=False, by_var=False, by_step=False, clip_values=True, \n",
    "                 use_single_batch=True, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.min = tensor(min) if min is not None else None\n",
    "        self.max = tensor(max) if max is not None else None\n",
    "        self._setup = (self.min is None and self.max is None) and not by_sample\n",
    "        self.range_min, self.range_max = range\n",
    "        self.by_sample, self.by_var, self.by_step = by_sample, by_var, by_step\n",
    "        drop_axes = []\n",
    "        if by_sample: drop_axes.append(0)\n",
    "        if by_var: drop_axes.append(1)\n",
    "        if by_step: drop_axes.append(2)\n",
    "        self.axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes])\n",
    "        if by_var and is_listy(by_var):\n",
    "            self.list_axes = tuple([ax for ax in (0, 1, 2) if ax not in drop_axes]) + (1,)\n",
    "        self.clip_values = clip_values\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.verbose = verbose\n",
    "        if self.min is not None or self.max is not None:\n",
    "            pv(f'{self.__class__.__name__} min={self.min}, max={self.max}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n', self.verbose)\n",
    "            \n",
    "    @classmethod\n",
    "    def from_stats(cls, min, max, range_min=0, range_max=1): return cls(min, max, range_min, range_max)\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                _min = torch.zeros(*shape, device=o.device) + self.range_min\n",
    "                _max = torch.zeros(*shape, device=o.device) + self.range_max\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    _min[:, v] = o[:, v].mul_min(self.axes if len(v) == 1 else self.list_axes, keepdim=self.axes!=())\n",
    "                    _max[:, v] = o[:, v].mul_max(self.axes if len(v) == 1 else self.list_axes, keepdim=self.axes!=())\n",
    "            else:\n",
    "                _min, _max = o.mul_min(self.axes, keepdim=self.axes!=()), o.mul_max(self.axes, keepdim=self.axes!=())\n",
    "            self.min, self.max = _min, _max\n",
    "            if len(self.min.shape) == 0: \n",
    "                pv(f'{self.__class__.__name__} min={self.min}, max={self.max}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n', \n",
    "                   self.verbose)\n",
    "            else:\n",
    "                pv(f'{self.__class__.__name__} min shape={self.min.shape}, max shape={self.max.shape}, by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step}\\n', \n",
    "                   self.verbose)\n",
    "            self._setup = False\n",
    "        elif self.by_sample: self.min, self.max = -torch.ones(1), torch.ones(1)\n",
    "\n",
    "    def encodes(self, o:TSTensor): \n",
    "        if self.by_sample: \n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                shape = torch.mean(o, dim=self.axes, keepdim=self.axes!=()).shape\n",
    "                _min = torch.zeros(*shape, device=o.device) + self.range_min\n",
    "                _max = torch.ones(*shape, device=o.device) + self.range_max\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    _min[:, v] = o[:, v].mul_min(self.axes, keepdim=self.axes!=())\n",
    "                    _max[:, v] = o[:, v].mul_max(self.axes, keepdim=self.axes!=())\n",
    "            else:\n",
    "                _min, _max = o.mul_min(self.axes, keepdim=self.axes!=()), o.mul_max(self.axes, keepdim=self.axes!=())\n",
    "            self.min, self.max = _min, _max\n",
    "        output = ((o - self.min) / (self.max - self.min)) * (self.range_max - self.range_min) + self.range_min\n",
    "        if self.clip_values:\n",
    "            if self.by_var and is_listy(self.by_var):\n",
    "                for v in self.by_var:\n",
    "                    if not is_listy(v): v = [v]\n",
    "                    output[:, v] = torch.clamp(output[:, v], self.range_min, self.range_max)\n",
    "            else:\n",
    "                output = torch.clamp(output, self.range_min, self.range_max)\n",
    "        return output\n",
    "    \n",
    "    def __repr__(self): return f'{self.__class__.__name__}(by_sample={self.by_sample}, by_var={self.by_var}, by_step={self.by_step})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms = [TSNormalize()]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb.max() <= 1\n",
    "assert xb.min() >= -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms=[TSNormalize(by_sample=True, by_var=False, verbose=False)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb.max() <= 1\n",
    "assert xb.min() >= -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_tfms = [TSNormalize(by_var=[0, [1, 2]], use_single_batch=False, clip_values=False, verbose=False)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb[:, [0, 1, 2]].max() <= 1\n",
    "assert xb[:, [0, 1, 2]].min() >= -1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSStandardizeTuple(ItemTransform):\n",
    "    \"Standardizes X (and y if provided)\"\n",
    "    parameters, order = L('x_mean', 'x_std', 'y_mean', 'y_std'), 90\n",
    "    \n",
    "    def __init__(self, x_mean, x_std, y_mean=None, y_std=None, eps=1e-5): \n",
    "        self.x_mean, self.x_std = torch.as_tensor(x_mean).float(), torch.as_tensor(x_std + eps).float()\n",
    "        self.y_mean = self.x_mean if y_mean is None else torch.as_tensor(y_mean).float()\n",
    "        self.y_std = self.x_std if y_std is None else torch.as_tensor(y_std + eps).float()\n",
    "        \n",
    "    def encodes(self, xy): \n",
    "        if len(xy) == 2:\n",
    "            x, y = xy\n",
    "            x = (x - self.x_mean) / self.x_std\n",
    "            y = (y - self.y_mean) / self.y_std\n",
    "            return (x, y)\n",
    "        elif len(xy) == 1:\n",
    "            x = xy[0]\n",
    "            x = (x - self.x_mean) / self.x_std\n",
    "            return (x, )\n",
    "    def decodes(self, xy): \n",
    "        if len(xy) == 2:\n",
    "            x, y = xy\n",
    "            x = x * self.x_std + self.x_mean\n",
    "            y = y * self.y_std + self.y_mean\n",
    "            return (x, y)\n",
    "        elif len(xy) == 1:\n",
    "            x = xy[0]\n",
    "            x = x * self.x_std + self.x_mean\n",
    "            return (x, )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a, b = TSTensor([1., 2, 3]), TSTensor([4., 5, 6])\n",
    "mean, std = a.mean(), b.std()\n",
    "tuple_batch_tfm = TSStandardizeTuple(mean, std)\n",
    "a_tfmd, b_tfmd = tuple_batch_tfm((a, b))\n",
    "test_ne(a, a_tfmd)\n",
    "test_ne(b, b_tfmd)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSCatEncode(Transform):\n",
    "    \"Encodes a variable based on a categorical array\"\n",
    "    def __init__(self, a, sel_var):\n",
    "        a_key = np.unique(a)\n",
    "        a_val = np.arange(1, len(a_key) + 1)\n",
    "        self.o2i = dict(zip(a_key, a_val))\n",
    "        self.a_key = torch.from_numpy(a_key)\n",
    "        self.sel_var = sel_var\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        o_ = o[:, self.sel_var]\n",
    "        o_val = torch.zeros_like(o_)\n",
    "        o_in_a = torch.isin(o_, self.a_key.to(o.device))\n",
    "        o_val[o_in_a] = o_[o_in_a].cpu().apply_(self.o2i.get).to(o.device) # apply is not available for cuda!!\n",
    "        o[:, self.sel_var] = o_val\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[0, 0, 0,  ..., 0, 0, 0],\n",
       "        [0, 0, 0,  ..., 0, 0, 0],\n",
       "        [0, 0, 0,  ..., 0, 0, 0],\n",
       "        ...,\n",
       "        [4, 4, 4,  ..., 4, 4, 4],\n",
       "        [4, 4, 4,  ..., 4, 4, 4],\n",
       "        [0, 0, 0,  ..., 0, 0, 0]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# static input\n",
    "a = np.random.randint(10, 20, 512)[:, None, None].repeat(10, 1).repeat(28, 2)\n",
    "b = TSTensor(torch.randint(0, 30, (512,), device='cpu').unsqueeze(-1).unsqueeze(-1).repeat(1, 10, 28))\n",
    "output = TSCatEncode(a, sel_var=0)(b)\n",
    "test_eq(0 <= output[:, 0].min() <= len(np.unique(a)), True)\n",
    "test_eq(0 <= output[:, 0].max() <= len(np.unique(a)), True)\n",
    "test_eq(output[:, 0], output[:, 0, 0][:, None].repeat(1, 28))\n",
    "output[:, 0].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[10,  0,  0,  ...,  4,  0,  0],\n",
       "        [10,  0,  0,  ...,  0,  0,  0],\n",
       "        [ 0,  2,  0,  ...,  0, 10,  6],\n",
       "        ...,\n",
       "        [ 1,  0,  9,  ...,  0,  0,  0],\n",
       "        [ 0,  0,  5,  ...,  0,  0,  0],\n",
       "        [ 0,  0,  0,  ...,  0,  0,  5]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# non-static input\n",
    "a = np.random.randint(10, 20, 512)[:, None, None].repeat(10, 1).repeat(28, 2)\n",
    "b = TSTensor(torch.randint(0, 30, (512, 10, 28), device='cpu'))\n",
    "output = TSCatEncode(a, sel_var=0)(b)\n",
    "test_eq(0 <= output[:, 0].min() <= len(np.unique(a)), True)\n",
    "test_eq(0 <= output[:, 0].max() <= len(np.unique(a)), True)\n",
    "test_ne(output[:, 0], output[:, 0, 0][:, None].repeat(1, 28))\n",
    "output[:, 0].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSDropFeatByKey(Transform):\n",
    "    \"\"\"Randomly drops selected features at selected steps based \n",
    "    with a given probability per feature, step and a key variable\"\"\"\n",
    "    parameters, order = 'p', 90\n",
    "    \n",
    "    def __init__(self, \n",
    "    key_var, # int representing the variable that contains the key information\n",
    "    p, # array of shape (n_keys, n_features, n_steps) representing the probabilities of dropping a feature at a given step for a given key\n",
    "    sel_vars, # int or slice or list of ints or array of ints representing the variables to drop\n",
    "    sel_steps=None, # int or slice or list of ints or array of ints representing the steps to drop\n",
    "    **kwargs,\n",
    "    ):\n",
    "        super().__init__(**kwargs)\n",
    "        if isinstance(p, np.ndarray):\n",
    "            p = torch.from_numpy(p)\n",
    "        if not isinstance(sel_vars, slice):\n",
    "            if isinstance(sel_vars, Integral): sel_vars = [sel_vars]\n",
    "            sel_vars = np.asarray(sel_vars)\n",
    "            if not isinstance(sel_steps, slice) and sel_steps is not None:\n",
    "                sel_vars = sel_vars.reshape(-1, 1)\n",
    "        if sel_steps is None:\n",
    "            sel_steps = slice(None)\n",
    "        elif not isinstance(sel_steps, slice):\n",
    "            if isinstance(sel_steps, Integral): sel_steps = [sel_steps]\n",
    "            sel_steps = np.asarray(sel_steps)\n",
    "            if not isinstance(sel_vars, slice):\n",
    "                sel_steps = sel_steps.reshape(1, -1)\n",
    "        self.key_var, self.p = key_var, p\n",
    "        self.sel_vars, self.sel_steps = sel_vars, sel_steps\n",
    "        if p.shape[-1] == 1:\n",
    "            if isinstance(self.sel_vars, slice) or isinstance(self.sel_steps, slice):\n",
    "                self._idxs = [slice(None), slice(None), slice(None), 0]\n",
    "            else:\n",
    "                self._idxs = [slice(None), 0, slice(None), slice(None), 0]\n",
    "        else:\n",
    "            if isinstance(self.sel_vars, slice) or isinstance(self.sel_steps, slice):\n",
    "                self._idxs = self._idxs = [slice(None), np.arange(p.shape[-1]), slice(None), np.arange(p.shape[-1])]\n",
    "            else:\n",
    "                self._idxs = [slice(None), 0, np.arange(p.shape[-1]), slice(None), np.arange(p.shape[-1])]\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        o_slice = o[:, self.sel_vars, self.sel_steps]\n",
    "        o_values = o[:, self.key_var, self.sel_steps]\n",
    "        o_values = torch.nan_to_num(o_values)\n",
    "        o_values = torch.round(o_values).long()\n",
    "        if self.p.shape[-1] == 1:\n",
    "            p = self.p[o_values][self._idxs].permute(0, 2, 1)\n",
    "        else:\n",
    "            p = self.p[o_values][self._idxs].permute(1, 2, 0)\n",
    "        mask = torch.rand_like(o_slice) < p\n",
    "        o_slice[mask] = np.nan\n",
    "        o[:, self.sel_vars, self.sel_steps] = o_slice\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_devices = 4\n",
    "key_var = 0\n",
    "\n",
    "for sel_vars in [1, [1], [1,3,5], slice(3, 5)]:\n",
    "    for sel_steps in [None, -1, 27, [27], [25, 26], slice(10, 20)]:\n",
    "        o = TSTensor(torch.rand(512, 10, 28))\n",
    "        o[:, key_var] = torch.randint(0, n_devices, (512, 28))\n",
    "        n_vars = 1 if isinstance(sel_vars, Integral) else len(sel_vars) if isinstance(sel_vars, list) else sel_vars.stop - sel_vars.start\n",
    "        n_steps = o.shape[-1] if sel_steps is None else 1 if isinstance(sel_steps, Integral) else \\\n",
    "            len(sel_steps) if isinstance(sel_steps, list) else sel_steps.stop - sel_steps.start\n",
    "        p = torch.rand(n_devices, n_vars, n_steps) * .5 + .5\n",
    "        output = TSDropFeatByKey(key_var, p, sel_vars, sel_steps)(o)\n",
    "        assert torch.isnan(output).sum((0, 2))[sel_vars].sum() > 0\n",
    "        assert torch.isnan(output).sum((0, 2))[~np.array(np.arange(o.shape[1])[sel_vars])].sum() == 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSClipOutliers(Transform):\n",
    "    \"Clip outliers batch of type `TSTensor` based on the IQR\"\n",
    "    parameters, order = L('min', 'max'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, min=None, max=None, by_sample=False, by_var=False, use_single_batch=False, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.min = tensor(min) if min is not None else tensor(-np.inf)\n",
    "        self.max = tensor(max) if max is not None else tensor(np.inf)\n",
    "        self.by_sample, self.by_var = by_sample, by_var\n",
    "        self._setup = (min is None or max is None) and not by_sample \n",
    "        if by_sample and by_var: self.axis = (2)\n",
    "        elif by_sample: self.axis = (1, 2)\n",
    "        elif by_var: self.axis = (0, 2)\n",
    "        else: self.axis = None\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.verbose = verbose\n",
    "        if min is not None or max is not None:\n",
    "            pv(f'{self.__class__.__name__} min={min}, max={max}\\n', self.verbose)\n",
    "\n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "            min, max = get_outliers_IQR(o, self.axis)\n",
    "            self.min, self.max = tensor(min), tensor(max)\n",
    "            if self.axis is None: pv(f'{self.__class__.__name__} min={self.min}, max={self.max}, by_sample={self.by_sample}, by_var={self.by_var}\\n', \n",
    "                                     self.verbose)\n",
    "            else: pv(f'{self.__class__.__name__} min={self.min.shape}, max={self.max.shape}, by_sample={self.by_sample}, by_var={self.by_var}\\n', \n",
    "                     self.verbose)\n",
    "            self._setup = False\n",
    "            \n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.axis is None: return torch.clamp(o, self.min, self.max)\n",
    "        elif self.by_sample: \n",
    "            min, max = get_outliers_IQR(o, axis=self.axis)\n",
    "            self.min, self.max = o.new(min), o.new(max)\n",
    "        return torch_clamp(o, self.min, self.max)\n",
    "    \n",
    "    def __repr__(self): return f'{self.__class__.__name__}(by_sample={self.by_sample}, by_var={self.by_var})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TSClipOutliers min=-1, max=1\n",
      "\n"
     ]
    }
   ],
   "source": [
    "batch_tfms=[TSClipOutliers(-1, 1, verbose=True)]\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, bs=128, num_workers=0, after_batch=batch_tfms)\n",
    "xb, yb = next(iter(dls.train))\n",
    "assert xb.max() <= 1\n",
    "assert xb.min() >= -1\n",
    "test_close(xb.min(), -1, eps=1e-1)\n",
    "test_close(xb.max(), 1, eps=1e-1)\n",
    "xb, yb = next(iter(dls.valid))\n",
    "test_close(xb.min(), -1, eps=1e-1)\n",
    "test_close(xb.max(), 1, eps=1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSClip(Transform):\n",
    "    \"Clip  batch of type `TSTensor`\"\n",
    "    parameters, order = L('min', 'max'), 90\n",
    "    def __init__(self, min=-6, max=6, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.min = torch.tensor(min)\n",
    "        self.max = torch.tensor(max)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch.clamp(o, self.min, self.max)\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(min={self.min}, max={self.max})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.randn(10, 20, 100)*10)\n",
    "test_le(TSClip()(t).max().item(), 6)\n",
    "test_ge(TSClip()(t).min().item(), -6)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSSelfMissingness(Transform):\n",
    "    \"Applies missingness from samples in a batch to random samples in the batch for selected variables\"\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, **kwargs):\n",
    "        self.sel_vars = sel_vars\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        if self.sel_vars is not None: \n",
    "            mask = rotate_axis0(torch.isnan(o[:, self.sel_vars]))\n",
    "            o[:, self.sel_vars] = o[:, self.sel_vars].masked_fill(mask, np.nan)\n",
    "        else:\n",
    "            mask = rotate_axis0(torch.isnan(o))\n",
    "            o.masked_fill_(mask, np.nan)\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.randn(10, 20, 100))\n",
    "t[t>.8] = np.nan\n",
    "t2 = TSSelfMissingness()(t.clone())\n",
    "t3 = TSSelfMissingness(sel_vars=[0,3,5,7])(t.clone())\n",
    "assert (torch.isnan(t).sum() < torch.isnan(t2).sum()) and (torch.isnan(t2).sum() >  torch.isnan(t3).sum())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSRobustScale(Transform):\n",
    "    r\"\"\"This Scaler removes the median and scales the data according to the quantile range (defaults to IQR: Interquartile Range)\"\"\"\n",
    "    parameters, order = L('median', 'iqr'), 90\n",
    "    _setup = True # indicates it requires set up\n",
    "    def __init__(self, median=None, iqr=None, quantile_range=(25.0, 75.0), use_single_batch=True, exc_vars=None, eps=1e-8, verbose=False, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.median = tensor(median) if median is not None else None\n",
    "        self.iqr = tensor(iqr) if iqr is not None else None\n",
    "        self._setup = median is None or iqr is None\n",
    "        self.use_single_batch = use_single_batch\n",
    "        self.exc_vars = exc_vars\n",
    "        self.eps = eps\n",
    "        self.verbose = verbose\n",
    "        self.quantile_range = quantile_range\n",
    "            \n",
    "    def setups(self, dl: DataLoader):\n",
    "        if self._setup:\n",
    "            if not self.use_single_batch:\n",
    "                o = dl.dataset.__getitem__([slice(None)])[0]\n",
    "            else:\n",
    "                o, *_ = dl.one_batch()\n",
    "\n",
    "            new_o = o.permute(1,0,2).flatten(1)\n",
    "            median = get_percentile(new_o, 50, axis=1)\n",
    "            iqrmin, iqrmax = get_outliers_IQR(new_o, axis=1, quantile_range=self.quantile_range)\n",
    "            self.median = median.unsqueeze(0)\n",
    "            self.iqr = torch.clamp_min((iqrmax - iqrmin).unsqueeze(0), self.eps)\n",
    "            if self.exc_vars is not None: \n",
    "                self.median[:, self.exc_vars] = 0\n",
    "                self.iqr[:, self.exc_vars] = 1\n",
    "            \n",
    "            pv(f'{self.__class__.__name__} median={self.median.shape} iqr={self.iqr.shape}', self.verbose)\n",
    "            self._setup = False\n",
    "        else: \n",
    "            if self.median is None: self.median = torch.zeros(1, device=dl.device)\n",
    "            if self.iqr is None: self.iqr = torch.ones(1, device=dl.device)\n",
    "\n",
    "            \n",
    "    def encodes(self, o:TSTensor):\n",
    "        return (o - self.median) / self.iqr\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(quantile_range={self.quantile_range}, use_single_batch={self.use_single_batch})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TSRobustScale median=torch.Size([1, 24, 1]) iqr=torch.Size([1, 24, 1])\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "TSTensor([-2.3502116203308105], device=cpu, dtype=torch.float32)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "batch_tfms = TSRobustScale(verbose=True, use_single_batch=False)\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, batch_tfms=batch_tfms, num_workers=0)\n",
    "xb, yb = next(iter(dls.train))\n",
    "xb.min()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([ 0.0000, -1.7305,  0.0000,  0.7365, -1.2736, -0.5528,  0.0000, -0.7074,\n",
      "         0.0000,  0.7087, -0.7014, -0.1120,  0.0000, -1.3332, -0.5958,  0.7563,\n",
      "        -1.0129, -0.3985, -0.5186, -1.5125, -0.7353,  0.7326, -1.1495, -0.5359])\n",
      "tensor([1.0000, 4.2788, 1.0000, 4.8008, 8.0682, 2.2777, 1.0000, 0.6955, 1.0000,\n",
      "        1.4875, 2.6386, 1.4756, 1.0000, 2.9811, 1.2507, 3.2291, 5.9906, 1.9098,\n",
      "        1.3428, 3.6368, 1.3689, 4.4213, 6.9907, 2.1939])\n"
     ]
    }
   ],
   "source": [
    "exc_vars = [0, 2, 6, 8, 12]\n",
    "batch_tfms = TSRobustScale(use_single_batch=False, exc_vars=exc_vars)\n",
    "dls = TSDataLoaders.from_dsets(dsets.train, dsets.valid, batch_tfms=batch_tfms, num_workers=0)\n",
    "xb, yb = next(iter(dls.train))\n",
    "test_eq(len(dls.train.after_batch.fs[0].median.flatten()), 24)\n",
    "test_eq(len(dls.train.after_batch.fs[0].iqr.flatten()), 24)\n",
    "test_eq(dls.train.after_batch.fs[0].median.flatten()[exc_vars].cpu(), torch.zeros(len(exc_vars)))\n",
    "test_eq(dls.train.after_batch.fs[0].iqr.flatten()[exc_vars].cpu(), torch.ones(len(exc_vars)))\n",
    "print(dls.train.after_batch.fs[0].median.flatten().data)\n",
    "print(dls.train.after_batch.fs[0].iqr.flatten().data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "def get_stats_with_uncertainty(o, sel_vars=None, sel_vars_zero_mean_unit_var=False, bs=64, n_trials=None, axis=(0,2)):\n",
    "    o_dtype = o.dtype\n",
    "    if n_trials is None: n_trials = len(o) // bs\n",
    "    random_idxs = random_choice(len(o), n_trials * bs, n_trials * bs > len(o))\n",
    "    oi_mean = []\n",
    "    oi_std = []\n",
    "    start = 0\n",
    "    for i in progress_bar(range(n_trials)):\n",
    "        idxs = random_idxs[start:start + bs]\n",
    "        start += bs\n",
    "        if hasattr(o, 'oindex'):\n",
    "            oi = o.index[idxs]\n",
    "        if hasattr(o, 'compute'):\n",
    "            oi = o[idxs].compute()\n",
    "        else:\n",
    "            oi = o[idxs]\n",
    "        oi_mean.append(np.nanmean(oi.astype('float32'), axis=axis, keepdims=True))\n",
    "        oi_std.append(np.nanstd(oi.astype('float32'), axis=axis, keepdims=True))\n",
    "    oi_mean = np.concatenate(oi_mean)\n",
    "    oi_std = np.concatenate(oi_std)\n",
    "    E_mean = np.nanmean(oi_mean, axis=0, keepdims=True).astype(o_dtype)\n",
    "    S_mean = np.nanstd(oi_mean, axis=0, keepdims=True).astype(o_dtype)\n",
    "    E_std = np.nanmean(oi_std, axis=0, keepdims=True).astype(o_dtype)\n",
    "    S_std = np.nanstd(oi_std, axis=0, keepdims=True).astype(o_dtype)\n",
    "    if sel_vars is not None:\n",
    "        non_sel_vars = np.isin(np.arange(o.shape[1]), sel_vars, invert=True)\n",
    "        if sel_vars_zero_mean_unit_var:\n",
    "            E_mean[:, non_sel_vars] = 0 # zero mean\n",
    "            E_std[:, non_sel_vars] = 1  # unit var\n",
    "        S_mean[:, non_sel_vars] = 0 # no uncertainty\n",
    "        S_std[:, non_sel_vars] = 0  # no uncertainty\n",
    "    return np.stack([E_mean, S_mean, E_std, S_std])\n",
    "\n",
    "\n",
    "def get_random_stats(E_mean, S_mean, E_std, S_std):\n",
    "    mult = np.random.normal(0, 1, 2)\n",
    "    new_mean = E_mean + S_mean * mult[0]\n",
    "    new_std = E_std + S_std * mult[1]\n",
    "    return new_mean, new_std\n",
    "\n",
    "\n",
    "class TSGaussianStandardize(Transform):\n",
    "    \"Scales each batch using modeled mean and std based on UNCERTAINTY MODELING FOR OUT-OF-DISTRIBUTION GENERALIZATION https://arxiv.org/abs/2202.03958\"\n",
    "\n",
    "    parameters, order = L('E_mean', 'S_mean', 'E_std', 'S_std'), 90\n",
    "    def __init__(self, \n",
    "        E_mean : np.ndarray, # Mean expected value\n",
    "        S_mean : np.ndarray, # Uncertainty (standard deviation) of the mean\n",
    "        E_std : np.ndarray,  # Standard deviation expected value\n",
    "        S_std : np.ndarray,  # Uncertainty (standard deviation) of the standard deviation\n",
    "        eps=1e-8, # (epsilon) small amount added to standard deviation to avoid deviding by zero\n",
    "        split_idx=0, # Flag to indicate to which set is this transofrm applied. 0: training, 1:validation, None:both\n",
    "        **kwargs,\n",
    "        ):\n",
    "        self.E_mean, self.S_mean = torch.from_numpy(E_mean), torch.from_numpy(S_mean)\n",
    "        self.E_std, self.S_std = torch.from_numpy(E_std), torch.from_numpy(S_std)\n",
    "        self.eps = eps\n",
    "        super().__init__(split_idx=split_idx, **kwargs)\n",
    "        \n",
    "    def encodes(self, o:TSTensor):\n",
    "        mult = torch.normal(0, 1, (2,), device=o.device)\n",
    "        new_mean = self.E_mean + self.S_mean * mult[0]\n",
    "        new_std = torch.clamp(self.E_std + self.S_std * mult[1], self.eps)\n",
    "        return (o - new_mean) / new_std\n",
    "    \n",
    "TSRandomStandardize = TSGaussianStandardize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<style>\n",
       "    /* Turns off some styling */\n",
       "    progress {\n",
       "        /* gets rid of default border in Firefox and Opera. */\n",
       "        border: none;\n",
       "        /* Needs to be in here for Safari polyfill so background images work as expected. */\n",
       "        background-size: auto;\n",
       "    }\n",
       "    progress:not([value]), progress:not([value])::-webkit-progress-bar {\n",
       "        background: repeating-linear-gradient(45deg, #7e7e7e, #7e7e7e 10px, #5c5c5c 10px, #5c5c5c 20px);\n",
       "    }\n",
       "    .progress-bar-interrupted, .progress-bar-interrupted::-webkit-progress-bar {\n",
       "        background: #F44336;\n",
       "    }\n",
       "</style>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    <div>\n",
       "      <progress value='15' class='' max='15' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
       "      100.00% [15/15 00:00&lt;00:00]\n",
       "    </div>\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "(array([[[0.49649504],\n",
       "         [0.49636062]]]),\n",
       " array([[[0.28626438],\n",
       "         [0.28665599]]]))"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "arr = np.random.rand(1000, 2, 50)\n",
    "E_mean, S_mean, E_std, S_std = get_stats_with_uncertainty(arr, sel_vars=None, bs=64, n_trials=None, axis=(0,2))\n",
    "new_mean, new_std = get_random_stats(E_mean, S_mean, E_std, S_std)\n",
    "new_mean2, new_std2 = get_random_stats(E_mean, S_mean, E_std, S_std)\n",
    "test_ne(new_mean, new_mean2)\n",
    "test_ne(new_std, new_std2)\n",
    "test_eq(new_mean.shape, (1, 2, 1))\n",
    "test_eq(new_std.shape, (1, 2, 1))\n",
    "new_mean, new_std"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "TSGaussianStandardize can be used jointly with TSStandardized in the following way: \n",
    "\n",
    "```python\n",
    "X, y, splits = get_UCR_data('LSST', split_data=False)\n",
    "tfms = [None, TSClassification()]\n",
    "E_mean, S_mean, E_std, S_std = get_stats_with_uncertainty(X, sel_vars=None, bs=64, n_trials=None, axis=(0,2))\n",
    "batch_tfms = [TSGaussianStandardize(E_mean, S_mean, E_std, S_std, split_idx=0), TSStandardize(E_mean, S_mean, split_idx=1)]\n",
    "dls = get_ts_dls(X, y, splits=splits, tfms=tfms, batch_tfms=batch_tfms, bs=[32, 64])\n",
    "learn = ts_learner(dls, InceptionTimePlus, metrics=accuracy, cbs=[ShowGraph()])\n",
    "learn.fit_one_cycle(1, 1e-2)\n",
    "```\n",
    "In this way the train batches are scaled based on mean and standard deviation distributions while the valid batches are scaled with a fixed mean and standard deviation values.\n",
    "\n",
    "The intent is to improve out-of-distribution performance. This method is inspired by UNCERTAINTY MODELING FOR OUT-OF-DISTRIBUTION GENERALIZATION https://arxiv.org/abs/2202.03958."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSDiff(Transform):\n",
    "    \"Differences batch of type `TSTensor`\"\n",
    "    order = 90\n",
    "    def __init__(self, lag=1, pad=True, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.lag, self.pad = lag, pad\n",
    "\n",
    "    def encodes(self, o:TSTensor): \n",
    "        return torch_diff(o, lag=self.lag, pad=self.pad)\n",
    "    \n",
    "    def __repr__(self): return f'{self.__class__.__name__}(lag={self.lag}, pad={self.pad})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.arange(24).reshape(2,3,4))\n",
    "test_eq(TSDiff()(t)[..., 1:].float().mean(), 1)\n",
    "test_eq(TSDiff(lag=2, pad=False)(t).float().mean(), 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSLog(Transform):\n",
    "    \"Log transforms batch of type `TSTensor` + 1. Accepts positive and negative numbers\"\n",
    "    order = 90\n",
    "    def __init__(self, ex=None, **kwargs):\n",
    "        self.ex = ex\n",
    "        super().__init__(**kwargs)\n",
    "    def encodes(self, o:TSTensor):\n",
    "        output = torch.zeros_like(o)\n",
    "        output[o > 0] = torch.log1p(o[o > 0])\n",
    "        output[o < 0] = -torch.log1p(torch.abs(o[o < 0]))\n",
    "        if self.ex is not None: output[...,self.ex,:] = o[...,self.ex,:]\n",
    "        return output\n",
    "    def decodes(self, o:TSTensor):\n",
    "        output = torch.zeros_like(o)\n",
    "        output[o > 0] = torch.exp(o[o > 0]) - 1\n",
    "        output[o < 0] = -torch.exp(torch.abs(o[o < 0])) + 1\n",
    "        if self.ex is not None: output[...,self.ex,:] = o[...,self.ex,:]\n",
    "        return output\n",
    "    def __repr__(self): return f'{self.__class__.__name__}()'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.rand(2,3,4)) * 2 - 1 \n",
    "tfm = TSLog()\n",
    "enc_t = tfm(t)\n",
    "test_ne(enc_t, t)\n",
    "test_close(tfm.decodes(enc_t).data, t.data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSCyclicalPosition(Transform):\n",
    "    \"Concatenates the position along the sequence as 2 additional variables (sine and cosine)\"\n",
    "    order = 90\n",
    "    def __init__(self, \n",
    "        cyclical_var=None, # Optional variable to indicate the steps withing the cycle (ie minute of the day)\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        drop_var=False, # Flag to indicate if the cyclical var is removed\n",
    "        **kwargs\n",
    "        ):\n",
    "        super().__init__(**kwargs)\n",
    "        self.cyclical_var, self.drop_var = cyclical_var, drop_var\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs,nvars,seq_len = o.shape\n",
    "        if self.cyclical_var is None:\n",
    "            sin, cos = sincos_encoding(seq_len, device=o.device)\n",
    "            output = torch.cat([o, sin.reshape(1,1,-1).repeat(bs,1,1), cos.reshape(1,1,-1).repeat(bs,1,1)], 1)\n",
    "            return output\n",
    "        else:\n",
    "            sin = torch.sin(o[:, [self.cyclical_var]]/seq_len * 2 * np.pi)\n",
    "            cos = torch.cos(o[:, [self.cyclical_var]]/seq_len * 2 * np.pi)\n",
    "            if self.drop_var:\n",
    "                exc_vars = np.isin(np.arange(nvars), self.cyclical_var, invert=True)\n",
    "                output = torch.cat([o[:, exc_vars], sin, cos], 1)\n",
    "            else:\n",
    "                output = torch.cat([o, sin, cos], 1)\n",
    "            return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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0dFReeONN5SjR48qixcvVtzd3ZUvv/yy+pg333xT8fX1VX788Udlz549yh133CFTl80wevRopVmzZtXTxH/44QeladOmynPPPVd9jLzW5snPz1d27typ7Ny5UwGU9957T9m5c6dy+vRpRVFq97oOHjxY6dq1q5KYmKhs2rRJad26tUwTt0YfffSREhERoTg7Oys9e/ZUtm7dqnUkmwdc8vL5559XH1NcXKw8/vjjip+fn+Lu7q7ceeedypkzZ7QLbUf+t8CR17r+/Pzzz0qHDh0UFxcXJTo6Wvnss89q3G4ymZSXX35ZCQoKUlxcXJQBAwYohw8f1iit7TIYDMpTTz2lREREKK6urkpUVJTy4osvKqWlpdXHyGttnrVr117y9/Po0aMVRand63r+/Hnl/vvvVzw9PRVvb29l7NixSn5+fr3k0ynKX5ZzFEIIIYSwAzIGRwghhBB2RwocIYQQQtgdKXCEEEIIYXekwBFCCCGE3ZECRwghhBB2RwocIYQQQtgdKXCEEEIIYXekwBFCCCGE3ZECRwghhBB2RwocIYQQQtgdKXCEEEIIYXekwBFCCCGE3fl/tq98um2JREEAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "enc_t = TSCyclicalPosition()(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1] - 2\n",
    "plt.plot(enc_t[0, -2:].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t1 = torch.rand(bs, c_in, seq_len)\n",
    "t2 = torch.arange(seq_len)\n",
    "t2 = torch.cat([t2[35:], t2[:35]]).reshape(1, 1, -1)\n",
    "t = TSTensor(torch.cat([t1, t2], 1))\n",
    "mask = torch.rand_like(t) > .8\n",
    "t[mask] = np.nan\n",
    "enc_t = TSCyclicalPosition(3)(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1] - 2\n",
    "plt.plot(enc_t[0, -2:].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSLinearPosition(Transform):\n",
    "    \"Concatenates the position along the sequence as 1 additional variable\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, \n",
    "        linear_var:int=None, # Optional variable to indicate the steps withing the cycle (ie minute of the day)\n",
    "        var_range:tuple=None, # Optional range indicating min and max values of the linear variable\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        drop_var:bool=False, # Flag to indicate if the cyclical var is removed\n",
    "        lin_range:tuple=(-1,1), \n",
    "        **kwargs): \n",
    "        self.linear_var, self.var_range, self.drop_var, self.lin_range = linear_var, var_range, drop_var, lin_range\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor): \n",
    "        bs,nvars,seq_len = o.shape\n",
    "        if self.linear_var is None:\n",
    "            lin = linear_encoding(seq_len, device=o.device, lin_range=self.lin_range)\n",
    "            output = torch.cat([o, lin.reshape(1,1,-1).repeat(bs,1,1)], 1)\n",
    "        else:\n",
    "            linear_var = o[:, [self.linear_var]]\n",
    "            if self.var_range is None:\n",
    "                lin = (linear_var - linear_var.min()) / (linear_var.max() - linear_var.min())\n",
    "            else:\n",
    "                lin = (linear_var - self.var_range[0]) / (self.var_range[1] - self.var_range[0])\n",
    "            lin = (linear_var - self.lin_range[0]) / (self.lin_range[1] - self.lin_range[0])\n",
    "            if self.drop_var:\n",
    "                exc_vars = np.isin(np.arange(nvars), self.linear_var, invert=True)\n",
    "                output = torch.cat([o[:, exc_vars], lin], 1)\n",
    "            else:\n",
    "                output = torch.cat([o, lin], 1)\n",
    "            return output\n",
    "        return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "enc_t = TSLinearPosition()(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1] - 1\n",
    "plt.plot(enc_t[0, -1].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "t = torch.arange(100)\n",
    "t1 = torch.cat([t[30:], t[:30]]).reshape(1, 1, -1)\n",
    "t2 = torch.cat([t[52:], t[:52]]).reshape(1, 1, -1)\n",
    "t = torch.cat([t1, t2]).float()\n",
    "mask = torch.rand_like(t) > .8\n",
    "t[mask] = np.nan\n",
    "t = TSTensor(t)\n",
    "enc_t = TSLinearPosition(linear_var=0, var_range=(0, 100), drop_var=True)(t)\n",
    "test_ne(enc_t, t)\n",
    "assert t.shape[1] == enc_t.shape[1]\n",
    "plt.plot(enc_t[0, -1].cpu().numpy().T)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSMissingness(Transform):\n",
    "    \"Concatenates data missingness for selected features along the sequence as additional variables\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, feature_idxs=None, magnitude=None, **kwargs):\n",
    "        sel_vars = sel_vars or feature_idxs\n",
    "        self.sel_vars = listify(sel_vars)\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        if self.sel_vars is not None:\n",
    "            missingness = o[:, self.sel_vars].isnan()\n",
    "        else:\n",
    "            missingness = o.isnan()\n",
    "        return torch.cat([o, missingness], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bs, c_in, seq_len = 1,3,100\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "t[t>.5] = np.nan\n",
    "enc_t = TSMissingness(sel_vars=[0,2])(t)\n",
    "test_eq(enc_t.shape[1], 5)\n",
    "test_eq(enc_t[:, 3:], torch.isnan(t[:, [0,2]]).float())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSPositionGaps(Transform):\n",
    "    \"\"\"Concatenates gaps for selected features along the sequence as additional variables\"\"\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, feature_idxs=None, magnitude=None, forward=True, backward=False, \n",
    "                 nearest=False, normalize=True, **kwargs):\n",
    "        sel_vars = sel_vars or feature_idxs\n",
    "        self.sel_vars = listify(sel_vars)\n",
    "        self.gap_fn = partial(get_gaps, forward=forward, backward=backward, nearest=nearest, normalize=normalize)\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        if self.sel_vars:\n",
    "            gaps = self.gap_fn(o[:, self.sel_vars])\n",
    "        else:\n",
    "            gaps = self.gap_fn(o)\n",
    "        return torch.cat([o, gaps], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[0.2875, 0.0553,    nan,    nan, 0.1478, 0.1234, 0.0835, 0.1465],\n",
       "         [   nan,    nan, 0.3967,    nan, 0.0654,    nan, 0.2289, 0.1094],\n",
       "         [0.3820, 0.1613, 0.4825, 0.1379,    nan,    nan, 0.3000, 0.4673],\n",
       "         [1.0000, 1.0000, 1.0000, 2.0000, 3.0000, 1.0000, 1.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 2.0000, 3.0000, 1.0000],\n",
       "         [1.0000, 3.0000, 2.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 3.0000, 2.0000, 1.0000, 1.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000],\n",
       "         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000, 1.0000]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,8\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "t[t>.5] = np.nan\n",
    "enc_t = TSPositionGaps(sel_vars=[0,2], forward=True, backward=True, nearest=True, normalize=False)(t)\n",
    "test_eq(enc_t.shape[1], 9)\n",
    "enc_t.data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSRollingMean(Transform):\n",
    "    \"\"\"Calculates the rolling mean for all/ selected features alongside the sequence\n",
    "    \n",
    "       It replaces the original values or adds additional variables (default)\n",
    "       If nan values are found, they will be filled forward and backward\"\"\"\n",
    "\n",
    "    order = 90\n",
    "    def __init__(self, sel_vars=None, feature_idxs=None, magnitude=None, window=2, replace=False, **kwargs):\n",
    "        sel_vars = sel_vars or feature_idxs\n",
    "        self.sel_vars = listify(sel_vars)\n",
    "        self.rolling_mean_fn = partial(rolling_moving_average, window=window)\n",
    "        self.replace = replace\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        if self.sel_vars:\n",
    "            if torch.isnan(o[:, self.sel_vars]).any():\n",
    "                o[:, self.sel_vars] = fbfill_sequence(o[:, self.sel_vars])\n",
    "            rolling_mean = self.rolling_mean_fn(o[:, self.sel_vars])\n",
    "            if self.replace: \n",
    "                o[:, self.sel_vars] = rolling_mean\n",
    "                return o\n",
    "        else:\n",
    "            if torch.isnan(o).any():\n",
    "                o = fbfill_sequence(o)\n",
    "            rolling_mean = self.rolling_mean_fn(o)\n",
    "            if self.replace: return rolling_mean\n",
    "        return torch.cat([o, rolling_mean], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[   nan, 0.3836,    nan,    nan, 0.0237, 0.4363,    nan, 0.1834],\n",
      "         [0.2749, 0.5018,    nan, 0.4008, 0.2797, 0.4010, 0.4323, 0.3692],\n",
      "         [0.4013,    nan, 0.1272, 0.2202, 0.4324, 0.3293, 0.5350, 0.3919]]])\n",
      "tensor([[[0.3836, 0.3836, 0.3836, 0.3836, 0.0237, 0.4363, 0.4363, 0.1834],\n",
      "         [0.2749, 0.5018,    nan, 0.4008, 0.2797, 0.4010, 0.4323, 0.3692],\n",
      "         [0.4013, 0.4013, 0.1272, 0.2202, 0.4324, 0.3293, 0.5350, 0.3919],\n",
      "         [0.3836, 0.3836, 0.3836, 0.3836, 0.2636, 0.2812, 0.2988, 0.3520],\n",
      "         [0.4013, 0.4013, 0.3099, 0.2496, 0.2599, 0.3273, 0.4322, 0.4187]]])\n",
      "tensor([[[0.3836, 0.3836, 0.3836, 0.3836, 0.2636, 0.2812, 0.2988, 0.3520],\n",
      "         [0.2749, 0.3883, 0.4261, 0.4681, 0.3941, 0.3605, 0.3710, 0.4008],\n",
      "         [0.4013, 0.4013, 0.3099, 0.2496, 0.2599, 0.3273, 0.4322, 0.4187]]])\n"
     ]
    }
   ],
   "source": [
    "bs, c_in, seq_len = 1,3,8\n",
    "t = TSTensor(torch.rand(bs, c_in, seq_len))\n",
    "t[t > .6] = np.nan\n",
    "print(t.data)\n",
    "enc_t = TSRollingMean(sel_vars=[0,2], window=3)(t)\n",
    "test_eq(enc_t.shape[1], 5)\n",
    "print(enc_t.data)\n",
    "enc_t = TSRollingMean(window=3, replace=True)(t)\n",
    "test_eq(enc_t.shape[1], 3)\n",
    "print(enc_t.data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSLogReturn(Transform):\n",
    "    \"Calculates log-return of batch of type `TSTensor`. For positive values only\"\n",
    "    order = 90\n",
    "    def __init__(self, lag=1, pad=True, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.lag, self.pad = lag, pad\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch_diff(torch.log(o), lag=self.lag, pad=self.pad)\n",
    "\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(lag={self.lag}, pad={self.pad})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor([1,2,4,8,16,32,64,128,256]).float()\n",
    "test_eq(TSLogReturn(pad=False)(t).std(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSAdd(Transform):\n",
    "    \"Add a defined amount to each batch of type `TSTensor`.\"\n",
    "    order = 90\n",
    "    def __init__(self, add, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.add = add\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return torch.add(o, self.add)\n",
    "    def __repr__(self): return f'{self.__class__.__name__}(lag={self.lag}, pad={self.pad})'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor([1,2,3]).float()\n",
    "test_eq(TSAdd(1)(t), TSTensor([2,3,4]).float())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSClipByVar(Transform):\n",
    "    \"\"\"Clip  batch of type `TSTensor` by variable\n",
    "    \n",
    "    Args:\n",
    "        var_min_max: list of tuples containing variable index, min value (or None) and max value (or None)\n",
    "    \"\"\"\n",
    "    order = 90\n",
    "    def __init__(self, var_min_max, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.var_min_max = var_min_max\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        for v,m,M in self.var_min_max:\n",
    "            o[:, v] = torch.clamp(o[:, v], m, M)\n",
    "        return o"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t = TSTensor(torch.rand(16, 3, 10) * tensor([1,10,100]).reshape(1,-1,1))\n",
    "max_values = t.max(0).values.max(-1).values.data\n",
    "max_values2 = TSClipByVar([(1,None,5), (2,10,50)])(t).max(0).values.max(-1).values.data\n",
    "test_le(max_values2[1], 5)\n",
    "test_ge(max_values2[2], 10)\n",
    "test_le(max_values2[2], 50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSDropVars(Transform):\n",
    "    \"Drops selected variable from the input\"\n",
    "    order = 90\n",
    "    def __init__(self, drop_vars, **kwargs):\n",
    "        super().__init__(**kwargs)\n",
    "        self.drop_vars = drop_vars\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        exc_vars = np.isin(np.arange(o.shape[1]), self.drop_vars, invert=True)\n",
    "        return o[:, exc_vars]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[ 0,  1,  2,  3],\n",
       "         [ 4,  5,  6,  7]],\n",
       "\n",
       "        [[12, 13, 14, 15],\n",
       "         [16, 17, 18, 19]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = TSTensor(torch.arange(24).reshape(2, 3, 4))\n",
    "enc_t = TSDropVars(2)(t)\n",
    "test_ne(t, enc_t)\n",
    "enc_t.data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSOneHotEncode(Transform):\n",
    "    order = 90\n",
    "    def __init__(self,\n",
    "        sel_var:int, # Variable that is one-hot encoded\n",
    "        unique_labels:list, # List containing all labels (excluding nan values)\n",
    "        add_na:bool=False, # Flag to indicate if values not included in vocab should be set as 0\n",
    "        drop_var:bool=True, # Flag to indicate if the selected var is removed\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        **kwargs\n",
    "        ):\n",
    "        unique_labels = listify(unique_labels)\n",
    "        self.sel_var = sel_var\n",
    "        self.unique_labels = unique_labels\n",
    "        self.n_classes = len(unique_labels) + add_na\n",
    "        self.add_na = add_na\n",
    "        self.drop_var = drop_var\n",
    "        super().__init__(**kwargs)\n",
    "        \n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs, n_vars, seq_len = o.shape\n",
    "        o_var = o[:, [self.sel_var]]\n",
    "        ohe_var = torch.zeros(bs, self.n_classes, seq_len, device=o.device)\n",
    "        if self.add_na:\n",
    "            is_na = torch.isin(o_var, o_var.new(list(self.unique_labels)), invert=True) # not available in dict\n",
    "            ohe_var[:, [0]] = is_na.to(ohe_var.dtype)\n",
    "        for i,l in enumerate(self.unique_labels):\n",
    "            ohe_var[:, [i + self.add_na]] = (o_var == l).to(ohe_var.dtype)\n",
    "        if self.drop_var:\n",
    "            exc_vars = torch.isin(torch.arange(o.shape[1], device=o.device), self.sel_var, invert=True)\n",
    "            output = torch.cat([o[:, exc_vars], ohe_var], 1)\n",
    "        else:\n",
    "            output = torch.cat([o, ohe_var], 1)\n",
    "        return output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[0, 2, 1, 0, 2]],\n",
       "\n",
       "        [[0, 0, 1, 1, 2]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs = 2\n",
    "seq_len = 5\n",
    "t_cont = torch.rand(bs, 1, seq_len)\n",
    "t_cat = torch.randint(0, 3, t_cont.shape)\n",
    "t = TSTensor(torch.cat([t_cat, t_cont], 1))\n",
    "t_cat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[1., 0., 0., 1., 0.],\n",
       "         [0., 0., 1., 0., 0.],\n",
       "         [0., 1., 0., 0., 1.]],\n",
       "\n",
       "        [[1., 1., 0., 0., 0.],\n",
       "         [0., 0., 1., 1., 0.],\n",
       "         [0., 0., 0., 0., 1.]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tfm = TSOneHotEncode(0, [0, 1, 2])\n",
    "output = tfm(t)[:, -3:].data\n",
    "test_eq(t_cat, torch.argmax(tfm(t)[:, -3:], 1)[:, None])\n",
    "tfm(t)[:, -3:].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[10.,  5., 11., nan, 12.]],\n",
       "\n",
       "        [[ 5., 12., 10., nan, 11.]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "bs = 2\n",
    "seq_len = 5\n",
    "t_cont = torch.rand(bs, 1, seq_len)\n",
    "t_cat = torch.tensor([[10.,  5., 11., np.nan, 12.], [ 5., 12., 10., np.nan, 11.]])[:, None]\n",
    "t = TSTensor(torch.cat([t_cat, t_cont], 1))\n",
    "t_cat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "tensor([[[1., 0., 0., 0., 0.],\n",
       "         [0., 0., 1., 0., 0.],\n",
       "         [0., 0., 0., 0., 1.]],\n",
       "\n",
       "        [[0., 0., 1., 0., 0.],\n",
       "         [0., 0., 0., 0., 1.],\n",
       "         [0., 1., 0., 0., 0.]]])"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tfm = TSOneHotEncode(0, [10, 11, 12], drop_var=False)\n",
    "mask = ~torch.isnan(t[:, 0])\n",
    "test_eq(tfm(t)[:, 0][mask], t[:, 0][mask])\n",
    "tfm(t)[:, -3:].data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "t1 = torch.randint(3, 7, (2, 1, 10))\n",
    "t2 = torch.rand(2, 1, 10)\n",
    "t = TSTensor(torch.cat([t1, t2], 1))\n",
    "output = TSOneHotEncode(0, [3, 4, 5], add_na=True, drop_var=True)(t)\n",
    "test_eq((t1 > 5).float(), output.data[:, [1]])\n",
    "test_eq((t1 == 3).float(), output.data[:, [2]])\n",
    "test_eq((t1 == 4).float(), output.data[:, [3]])\n",
    "test_eq((t1 == 5).float(), output.data[:, [4]])\n",
    "test_eq(output.shape, (t.shape[0], 5, t.shape[-1]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSPosition(Transform):\n",
    "    order = 90\n",
    "    def __init__(self,\n",
    "        steps:list, # List containing the steps passed as an additional variable. Theu should be normalized.\n",
    "        magnitude=None, # Added for compatibility. It's not used.\n",
    "        **kwargs\n",
    "        ):\n",
    "        self.steps = torch.from_numpy(np.asarray(steps)).reshape(1, 1, -1)\n",
    "        super().__init__(**kwargs)\n",
    "\n",
    "    def encodes(self, o: TSTensor):\n",
    "        bs = o.shape[0]\n",
    "        steps = self.steps.expand(bs, -1, -1).to(device=o.device, dtype=o.dtype)\n",
    "        return torch.cat([o, steps], 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(torch.float32, torch.float32)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "t = TSTensor(torch.rand(2, 1, 10)).float()\n",
    "a = np.linspace(-1, 1, 10).astype('float64')\n",
    "TSPosition(a)(t).data.dtype, t.dtype"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "import torch\n",
    "import torch.nn.functional as F\n",
    "\n",
    "class PatchEncoder():\n",
    "    \"Creates a sequence of patches from a 3d input tensor.\"\n",
    "\n",
    "    def __init__(self, \n",
    "        patch_len:int, # Number of time steps in each patch.\n",
    "        patch_stride:int=None, # Stride of the patch.\n",
    "        pad_at_start:bool=True, # If True, pad the input tensor at the start to ensure that the input tensor is evenly divisible by the patch length.\n",
    "        value:float=0.0, # Value to pad the input tensor with.\n",
    "        seq_len:int=None, # Number of time steps in the input tensor. If None, make sure seq_len >= patch_len and a multiple of stride\n",
    "        merge_dims:bool=True, # If True, merge channels within the same patch.\n",
    "        reduction:str='none', # type of reduction applied. Available: \"none\", \"mean\", \"min\", \"max\", \"mode\"\n",
    "        reduction_dim:int=-1, # dimension where the reduction is applied\n",
    "        swap_dims:tuple=None, # If True, swap the time and channel dimensions.\n",
    "        ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.seq_len = seq_len\n",
    "        self.patch_len = patch_len\n",
    "        self.patch_stride = patch_stride or patch_len\n",
    "        self.pad_at_start = pad_at_start\n",
    "        self.value = value\n",
    "        self.merge_dims = merge_dims\n",
    "\n",
    "        assert reduction in [\"none\", \"mean\", \"min\", \"max\", \"mode\"]\n",
    "        self.reduction = reduction\n",
    "        self.reduction_dim = reduction_dim\n",
    "        self.swap_dims = swap_dims\n",
    "        \n",
    "        if seq_len is None:\n",
    "            self.pad_size = 0\n",
    "        elif self.seq_len < self.patch_len:\n",
    "            self.pad_size = self.patch_len - self.seq_len\n",
    "        else:\n",
    "            if (self.seq_len % self.patch_len) % self.patch_stride == 0:\n",
    "                self.pad_size = 0\n",
    "            else:\n",
    "                self.pad_size = self.patch_stride - (self.seq_len % self.patch_len) % self.patch_stride\n",
    "\n",
    "    def __call__(self, \n",
    "        x: torch.Tensor # 3d input tensor with shape [batch size, sequence length, channels]\n",
    "        ) -> torch.Tensor: #  Transformed tensor of patches with shape [batch size, channels*patch length, number of patches]\n",
    "\n",
    "        if x.ndim == 2:\n",
    "            x = x[:, None]\n",
    "            \n",
    "        bs, c_in, *_ = x.size()\n",
    "        if not bs: \n",
    "            return x\n",
    "        \n",
    "        if self.pad_size:\n",
    "            x = F.pad(x, (self.pad_size, 0), value=self.value) if self.pad_at_start else F.pad(x, (0, self.pad_size), value=self.value)\n",
    "        \n",
    "        x = x.unfold(2, self.patch_len, self.patch_stride)\n",
    "        x = x.permute(0, 1, 3, 2)\n",
    "        if self.merge_dims:\n",
    "            x = x.reshape(bs, c_in * self.patch_len, -1)\n",
    "\n",
    "        if self.reduction == \"mean\":\n",
    "            x = x.mean(self.reduction_dim)\n",
    "        elif self.reduction == \"min\":\n",
    "            x = x.min(self.reduction_dim).values\n",
    "        elif self.reduction == \"max\":\n",
    "            x = x.max(self.reduction_dim).values\n",
    "        elif self.reduction == \"mode\":\n",
    "            x = x.mode(self.reduction_dim).values\n",
    "\n",
    "        if self.swap_dims:\n",
    "            x = x.swapaxes(*self.swap_dims)\n",
    "\n",
    "        return x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([3, 2, 17]) \n",
      "\n",
      "tensor([[[  0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,\n",
      "           14,  15,  16],\n",
      "         [  0,  10,  20,  30,  40,  50,  60,  70,  80,  90, 100, 110, 120, 130,\n",
      "          140, 150, 160]],\n",
      "\n",
      "        [[  1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,\n",
      "           15,  16,  17],\n",
      "         [  1,  11,  21,  31,  41,  51,  61,  71,  81,  91, 101, 111, 121, 131,\n",
      "          141, 151, 161]],\n",
      "\n",
      "        [[  2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,\n",
      "           16,  17,  18],\n",
      "         [  2,  12,  22,  32,  42,  52,  62,  72,  82,  92, 102, 112, 122, 132,\n",
      "          142, 152, 162]]])\n",
      "torch.Size([3, 20, 3]) \n",
      "\n"
     ]
    }
   ],
   "source": [
    "seq_len = 17\n",
    "patch_len = 10\n",
    "patch_stride = 5\n",
    "\n",
    "z11 = torch.arange(seq_len).reshape(1, 1, -1)\n",
    "z12 = torch.arange(seq_len).reshape(1, 1, -1) * 10\n",
    "z1 = torch.cat((z11, z12), dim=1)\n",
    "z21 = torch.arange(seq_len).reshape(1, 1, -1)\n",
    "z22 = torch.arange(seq_len).reshape(1, 1, -1) * 10\n",
    "z2 = torch.cat((z21, z22), dim=1) + 1\n",
    "z31 = torch.arange(seq_len).reshape(1, 1, -1)\n",
    "z32 = torch.arange(seq_len).reshape(1, 1, -1) * 10\n",
    "z3 = torch.cat((z31, z32), dim=1) + 2\n",
    "z = torch.cat((z11, z21, z31), dim=0)\n",
    "z = torch.cat((z1, z2, z3), dim=0)\n",
    "print(z.shape, \"\\n\")\n",
    "print(z)\n",
    "\n",
    "patch_encoder = PatchEncoder(patch_len=patch_len, patch_stride=patch_stride, value=-1, seq_len=seq_len, merge_dims=True)\n",
    "output = patch_encoder(z)\n",
    "print(output.shape, \"\\n\")\n",
    "first_token = output[..., 0]\n",
    "expected_first_token = torch.tensor([[-1, -1, -1,  0,  1,  2,  3,  4,  5,  6, -1, -1, -1,  0, 10, 20, 30, 40,\n",
    "         50, 60],\n",
    "        [-1, -1, -1,  1,  2,  3,  4,  5,  6,  7, -1, -1, -1,  1, 11, 21, 31, 41,\n",
    "         51, 61],\n",
    "        [-1, -1, -1,  2,  3,  4,  5,  6,  7,  8, -1, -1, -1,  2, 12, 22, 32, 42,\n",
    "         52, 62]])\n",
    "test_eq(first_token, expected_first_token)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSPatchEncoder(Transform):\n",
    "    \"Tansforms a time series into a sequence of patches along the last dimension\"\n",
    "    order = 90\n",
    "    \n",
    "    def __init__(self, \n",
    "        patch_len:int, # Number of time steps in each patch.\n",
    "        patch_stride:int=None, # Stride of the patch.\n",
    "        pad_at_start:bool=True, # If True, pad the input tensor at the start to ensure that the input tensor is evenly divisible by the patch length.\n",
    "        value:float=0.0, # Value to pad the input tensor with.\n",
    "        seq_len:int=None, # Number of time steps in the input tensor. If None, make sure seq_len >= patch_len and a multiple of stride\n",
    "        merge_dims:bool=True, # If True, merge channels within the same patch.\n",
    "        reduction:str='none', # type of reduction applied. Available: \"none\", \"mean\", \"min\", \"max\", \"mode\"\n",
    "        reduction_dim:int=-2, # dimension where the y reduction is applied.\n",
    "        swap_dims:tuple=None, # If True, swap the time and channel dimensions.\n",
    "        ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.patch_encoder = PatchEncoder(patch_len=patch_len, \n",
    "                                          patch_stride=patch_stride, \n",
    "                                          pad_at_start=pad_at_start, \n",
    "                                          value=value,\n",
    "                                          seq_len=seq_len, \n",
    "                                          merge_dims=merge_dims,\n",
    "                                          reduction=reduction,\n",
    "                                          reduction_dim=reduction_dim,\n",
    "                                          swap_dims=swap_dims)\n",
    "\n",
    "    def encodes(self, o:TSTensor):\n",
    "        return self.patch_encoder(o)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9]],\n",
      "\n",
      "        [[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]])\n",
      "torch.Size([2, 1, 10]) \n",
      "\n",
      "first patch:\n",
      " tensor([[ 0,  1,  2,  3],\n",
      "        [10, 11, 12, 13]]) \n",
      "\n",
      "first patch:\n",
      " tensor([[ 0,  0,  0,  1],\n",
      "        [ 0,  0, 10, 11]]) \n",
      "\n"
     ]
    }
   ],
   "source": [
    "bs = 2\n",
    "c_in = 1\n",
    "seq_len = 10\n",
    "patch_len = 4\n",
    "\n",
    "t = TSTensor(torch.arange(bs * c_in * seq_len).reshape(bs, c_in, seq_len))\n",
    "print(t.data)\n",
    "print(t.shape, \"\\n\")\n",
    "\n",
    "patch_encoder = TSPatchEncoder(patch_len=patch_len, patch_stride=1, seq_len=seq_len)\n",
    "output = patch_encoder(t)\n",
    "test_eq(output.shape, ([bs, patch_len, 7]))\n",
    "print(\"first patch:\\n\", output[..., 0].data, \"\\n\")\n",
    "\n",
    "patch_encoder = TSPatchEncoder(patch_len=patch_len, patch_stride=None, seq_len=seq_len)\n",
    "output = patch_encoder(t)\n",
    "test_eq(output.shape, ([bs, patch_len, 3]))\n",
    "print(\"first patch:\\n\", output[..., 0].data, \"\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from fastcore.transform import ItemTransform\n",
    "\n",
    "class TSTuplePatchEncoder(ItemTransform):\n",
    "    \"Tansforms a time series with x and y into sequences of patches along the last dimension\"\n",
    "    order = 90\n",
    "    \n",
    "    def __init__(self, \n",
    "        patch_len:int, # Number of time steps in each patch.\n",
    "        patch_stride:int=None, # Stride of the patch.\n",
    "        pad_at_start:bool=True, # If True, pad the input tensor at the start to ensure that the input tensor is evenly divisible by the patch length.\n",
    "        value:float=0.0, # Value to pad the input tensor with.\n",
    "        seq_len:int=None, # Number of time steps in the input tensor. If None, make sure seq_len >= patch_len and a multiple of stride\n",
    "        merge_dims:bool=True, # If True, merge y channels within the same patch.\n",
    "        reduction:str='none', # type of reduction applied to y. Available: \"none\", \"mean\", \"min\", \"max\", \"mode\"\n",
    "        reduction_dim:int=-2, # dimension where the y reduction is applied.\n",
    "        swap_dims:tuple=None, # If True, swap the time and channel dimensions in y.\n",
    "        ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.x_patch_encoder = PatchEncoder(patch_len=patch_len, \n",
    "                                            patch_stride=patch_stride, \n",
    "                                            pad_at_start=pad_at_start, \n",
    "                                            value=value,\n",
    "                                            seq_len=seq_len)\n",
    "\n",
    "        self.y_patch_encoder = PatchEncoder(patch_len=patch_len, \n",
    "                                            patch_stride=patch_stride, \n",
    "                                            pad_at_start=pad_at_start, \n",
    "                                            value=value,\n",
    "                                            seq_len=seq_len, \n",
    "                                            merge_dims=merge_dims,\n",
    "                                            reduction=reduction,\n",
    "                                            reduction_dim=reduction_dim,\n",
    "                                            swap_dims=swap_dims)\n",
    "\n",
    "    def encodes(self, xy):\n",
    "        if len(xy) == 2:\n",
    "            x, y = xy\n",
    "            x, y = self.x_patch_encoder(x), self.y_patch_encoder(y)\n",
    "            return (x, y)\n",
    "        elif len(xy) == 1:\n",
    "            x = xy[0]\n",
    "            x = self.x_patch_encoder(x)\n",
    "            return (x, )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor([[[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],\n",
      "         [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]],\n",
      "\n",
      "        [[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],\n",
      "         [30, 31, 32, 33, 34, 35, 36, 37, 38, 39]]])\n",
      "tensor([[[  0,  10,  20,  30,  40,  50,  60,  70,  80,  90],\n",
      "         [100, 110, 120, 130, 140, 150, 160, 170, 180, 190]],\n",
      "\n",
      "        [[200, 210, 220, 230, 240, 250, 260, 270, 280, 290],\n",
      "         [300, 310, 320, 330, 340, 350, 360, 370, 380, 390]]])\n",
      "first x patch:\n",
      " tensor([[ 0,  1,  2,  3, 10, 11, 12, 13],\n",
      "        [20, 21, 22, 23, 30, 31, 32, 33]]) \n",
      "\n",
      "first y patch:\n",
      " tensor([[  0,  10,  20,  30, 100, 110, 120, 130],\n",
      "        [200, 210, 220, 230, 300, 310, 320, 330]]) \n",
      "\n",
      "first x patch:\n",
      " tensor([[ 0,  1,  2,  3, 10, 11, 12, 13],\n",
      "        [20, 21, 22, 23, 30, 31, 32, 33]]) \n",
      "\n",
      "first y patch:\n",
      " tensor([[ 30, 130],\n",
      "        [230, 330]]) \n",
      "\n"
     ]
    }
   ],
   "source": [
    "# test\n",
    "bs = 2\n",
    "c_in = 2\n",
    "seq_len = 10\n",
    "patch_len = 4\n",
    "\n",
    "x = torch.arange(bs * c_in * seq_len).reshape(bs, c_in, seq_len)\n",
    "y = torch.arange(bs * c_in * seq_len).reshape(bs, c_in, seq_len) * 10\n",
    "print(x)\n",
    "print(y)\n",
    "\n",
    "\n",
    "patch_encoder = TSTuplePatchEncoder(patch_len=patch_len, patch_stride=1, seq_len=seq_len, merge_dims=True)\n",
    "x_out, y_out = patch_encoder((x, y))\n",
    "test_eq(x_out.shape, ([bs, c_in * patch_len, 7]))\n",
    "test_eq(y_out.shape, ([bs, c_in * patch_len, 7]))\n",
    "print(\"first x patch:\\n\", x_out[..., 0].data, \"\\n\")\n",
    "print(\"first y patch:\\n\", y_out[..., 0].data, \"\\n\")\n",
    "\n",
    "patch_encoder = TSTuplePatchEncoder(patch_len=patch_len, patch_stride=1, seq_len=seq_len, merge_dims=False, reduction=\"max\")\n",
    "x_out, y_out = patch_encoder((x, y))\n",
    "test_eq(x_out.shape, ([bs, c_in * patch_len, 7]))\n",
    "test_eq(y_out.shape, ([bs, c_in, 7]))\n",
    "print(\"first x patch:\\n\", x_out[..., 0].data, \"\\n\")\n",
    "print(\"first y patch:\\n\", y_out[..., 0].data, \"\\n\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# sklearn API transforms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSShrinkDataFrame(BaseEstimator, TransformerMixin):\n",
    "    \"\"\"A transformer to shrink dataframe or series memory usage\"\"\"\n",
    "\n",
    "    def __init__(self, \n",
    "        columns=None, # List[str], optional. Columns to shrink, all columns by default.\n",
    "        skip=None, # List[str], optional. Columns to skip, None by default.\n",
    "        obj2cat=True, # bool, optional. Convert object columns to category, True by default.\n",
    "        int2uint=False, # bool, optional. Convert int columns to uint, False by default.\n",
    "        verbose=True # bool, optional. Print memory usage info. True by default.\n",
    "        ):\n",
    "        self.columns, self.skip, self.obj2cat, self.int2uint, self.verbose = listify(columns), listify(skip), obj2cat, int2uint, verbose\n",
    "        \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        if isinstance(X, pd.Series): \n",
    "            X = X.to_frame()\n",
    "        assert isinstance(X, pd.DataFrame), \"X must be a pd.DataFrame or pd.Series\" \n",
    "        if isinstance(X, pd.Series):\n",
    "            X = X.to_frame().apply(object2date)\n",
    "        else:\n",
    "            X = X.apply(object2date)\n",
    "        if self.columns:\n",
    "            self.dt = df_shrink_dtypes(X[self.columns], self.skip, obj2cat=self.obj2cat, int2uint=self.int2uint)\n",
    "        else:\n",
    "            self.dt = df_shrink_dtypes(X, self.skip, obj2cat=self.obj2cat, int2uint=self.int2uint)\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        if isinstance(X, pd.Series): \n",
    "            col_name = X.name\n",
    "            X = X.to_frame()\n",
    "        else:\n",
    "            col_name = None\n",
    "        assert isinstance(X, pd.DataFrame), \"X must be a pd.DataFrame or pd.Series\"\n",
    "        if self.verbose:\n",
    "            start_memory = X.memory_usage().sum()\n",
    "            print(f\"Initial memory usage: {bytes2str(start_memory):10}\")\n",
    "        if isinstance(X, pd.Series):\n",
    "            X = X.to_frame().apply(object2date)\n",
    "        else:\n",
    "            X = X.apply(object2date)\n",
    "        if self.columns:\n",
    "            X.loc[:, self.columns] = X[self.columns].astype(self.dt)\n",
    "        else:\n",
    "            X = X.astype(self.dt)\n",
    "        if self.verbose:\n",
    "            end_memory = X.memory_usage().sum()\n",
    "            print(f\"Final memory usage  : {bytes2str(end_memory):10} ({(end_memory - start_memory) / start_memory:.1%})\")\n",
    "        if col_name is not None:\n",
    "            X = X[col_name]\n",
    "        return X\n",
    "         \n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X\n",
    "\n",
    "\n",
    "def object2date(x, format=None):\n",
    "    if not x.dtype == np.dtype('object'): return x\n",
    "    try:\n",
    "        return pd.to_datetime(x, format=format)\n",
    "    except:\n",
    "        return x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial memory usage: 288.00 B  \n",
      "Final memory usage  : 178.00 B   (-38.2%)\n"
     ]
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"ints64\"] = np.random.randint(0,3,10)\n",
    "df['floats64'] = np.random.rand(10)\n",
    "tfm = TSShrinkDataFrame()\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df[\"ints64\"].dtype, \"int8\")\n",
    "test_eq(df[\"floats64\"].dtype, \"float32\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial memory usage: 368.00 B  \n",
      "Final memory usage  : 258.00 B   (-29.9%)\n"
     ]
    }
   ],
   "source": [
    "# test with date\n",
    "df = pd.DataFrame()\n",
    "df[\"dates\"] = pd.date_range('1/1/2011', periods=10, freq='M').astype(str)\n",
    "df[\"ints64\"] = np.random.randint(0,3,10)\n",
    "df['floats64'] = np.random.rand(10)\n",
    "tfm = TSShrinkDataFrame()\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df[\"dates\"].dtype, \"datetime64[ns]\")\n",
    "test_eq(df[\"ints64\"].dtype, \"int8\")\n",
    "test_eq(df[\"floats64\"].dtype, \"float32\")\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Initial memory usage: 208.00 B  \n",
      "Final memory usage  : 208.00 B   (0.0%)\n"
     ]
    }
   ],
   "source": [
    "# test with date and series\n",
    "df = pd.DataFrame()\n",
    "df[\"dates\"] = pd.date_range('1/1/2011', periods=10, freq='M').astype(str)\n",
    "tfm = TSShrinkDataFrame()\n",
    "df = tfm.fit_transform(df[\"dates\"])\n",
    "test_eq(df.dtype, \"datetime64[ns]\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSOneHotEncoder(BaseEstimator, TransformerMixin):\n",
    "    \"Encode categorical variables using one-hot encoding\"\n",
    "\n",
    "    def __init__(\n",
    "        self,\n",
    "        columns=None,  # (str or List[str], optional): Column name(s) to encode. If None, all columns will be encoded. Defaults to None.\n",
    "        drop=True,  # (bool, optional): Whether to drop the original columns after encoding. Defaults to True.\n",
    "        add_na=True,  # (bool, optional): Whether to add a 'NaN' category for missing values. Defaults to True.\n",
    "        dtype=np.int8,  # (type, optional): Data type of the encoded output. Defaults to np.int64.\n",
    "    ):\n",
    "        self.columns = listify(columns)\n",
    "        self.drop, self.add_na, self.dtype = drop, add_na, dtype\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        handle_unknown = \"ignore\" if self.add_na else \"error\"\n",
    "        self.ohe_tfm = sklearn.preprocessing.OneHotEncoder(handle_unknown=handle_unknown)\n",
    "        self.dtypes = [X[c].dtype for c in self.columns]\n",
    "        if len(self.columns) == 1:\n",
    "            self.ohe_tfm.fit(X[self.columns].to_numpy().reshape(-1, 1))\n",
    "        else:\n",
    "            self.ohe_tfm.fit(X[self.columns])\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if len(self.columns) == 1:\n",
    "            output = self.ohe_tfm.transform(X[self.columns].to_numpy().reshape(-1, 1)).toarray().astype(self.dtype)\n",
    "        else:\n",
    "            output = self.ohe_tfm.transform(X[self.columns]).toarray().astype(self.dtype)\n",
    "        new_cols = []\n",
    "        for i,col in enumerate(self.columns):\n",
    "            for cats in self.ohe_tfm.categories_[i]:\n",
    "                new_cols.append(f\"{str(col)}_{str(cats)}\")\n",
    "        X[new_cols] = output\n",
    "        self.new_cols = new_cols\n",
    "        if self.drop: X = X.drop(self.columns, axis=1)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        if len(self.new_cols) == 1:\n",
    "            output = self.ohe_tfm.inverse_transform(X[self.new_cols].to_numpy().reshape(-1, 1))\n",
    "        else:\n",
    "            output = self.ohe_tfm.inverse_transform(X[self.new_cols])\n",
    "        for i,(col,d) in enumerate(zip(self.columns, self.dtypes)):\n",
    "            X[col] = output[:, i]\n",
    "            if hasattr(d, \"categories\"):\n",
    "                X[col] = X[col].astype('category')\n",
    "        if self.drop:\n",
    "            X = X.drop(self.new_cols, axis=1)\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = np.random.randint(0,2,10)\n",
    "df[\"b\"] = np.random.randint(0,3,10)\n",
    "unique_cols = len(df[\"a\"].unique()) + len(df[\"b\"].unique())\n",
    "tfm = TSOneHotEncoder()\n",
    "tfm.fit(df)\n",
    "df = tfm.transform(df)\n",
    "test_eq(df.shape[1], unique_cols)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSCategoricalEncoder(BaseEstimator, TransformerMixin):\n",
    "    \"\"\"A transformer to encode categorical columns\"\"\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, # List[str], optional. Columns to encode, all columns by default.\n",
    "        add_na=True, # bool, optional. Add a NaN category, True by default.\n",
    "        sort=True, # bool, optional. Sort categories by frequency, True by default.\n",
    "        categories='auto', # dict, optional. The custom mapping of categories. 'auto' by default.\n",
    "        inplace=True, # bool, optional. Modify input DataFrame, True by default.\n",
    "        prefix=None, # str, optional. Prefix for created column names. None by default.\n",
    "        suffix=None, # str, optional. Suffix for created column names. None by default.\n",
    "        drop=False # bool, optional. Drop original columns, False by default.\n",
    "        ):\n",
    "        self.columns = listify(columns)\n",
    "        self.add_na = add_na\n",
    "        self.prefix = prefix\n",
    "        self.suffix = suffix\n",
    "        self.sort = sort\n",
    "        self.inplace = inplace\n",
    "        self.drop = drop\n",
    "        if categories is None or categories == 'auto': \n",
    "            self.categories = None\n",
    "        else:\n",
    "            assert is_listy(categories) and len(categories) > 0, \"you must pass a list or list of lists of categories\"\n",
    "            self.categories = self.to_categorical(categories)\n",
    "\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        \n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        if self.categories is None:\n",
    "            _categories = []\n",
    "            for column in self.columns:\n",
    "                if isinstance(X, pd.DataFrame) and hasattr(X[column], \"cat\"):\n",
    "                    categories = X[column].cat.categories\n",
    "                elif hasattr(X, \"cat\"):\n",
    "                    categories = X.cat.categories\n",
    "                else:\n",
    "                    categories = X.loc[idxs, column].dropna().unique() if isinstance(X, pd.DataFrame) else X[idxs].dropna().unique()\n",
    "                    if self.sort:\n",
    "                        categories = np.sort(categories)\n",
    "                categories = pd.CategoricalDtype(categories=categories, ordered=True)\n",
    "                _categories.append(categories)\n",
    "            self.categories = _categories\n",
    "        assert len(self.categories) == len(self.columns)\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if isinstance(X, pd.DataFrame):\n",
    "            columns = X.columns\n",
    "        else:\n",
    "            columns = X.name\n",
    "        for column, categories in zip(self.columns, self.categories):\n",
    "            if column not in columns:\n",
    "                continue\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                name = []\n",
    "                if self.prefix: name += [self.prefix]\n",
    "                name += [column]\n",
    "                if self.suffix: name += [self.suffix]\n",
    "                new_col = '_'.join(name)\n",
    "                if self.drop:\n",
    "                    X.loc[:, column] = X.loc[:, column].astype(categories).cat.codes + self.add_na\n",
    "                    X.rename(columns={column: new_col}, inplace=True)\n",
    "                else:\n",
    "                    X.loc[:, new_col] = X.loc[:, column].astype(categories).cat.codes + self.add_na\n",
    "            else:\n",
    "                X = X.astype(categories).cat.codes + self.add_na\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if isinstance(X, pd.DataFrame):\n",
    "            columns = X.columns\n",
    "            for column, categories in zip(self.columns, self.categories):\n",
    "                if column not in columns:\n",
    "                    continue\n",
    "                name = []\n",
    "                if self.prefix: name += [self.prefix]\n",
    "                name += [column]\n",
    "                if self.suffix: name += [self.suffix]\n",
    "                new_col = '_'.join(name)\n",
    "                if self.add_na:\n",
    "                    X.loc[:, new_col] = np.array(['#na#'] + list(categories.categories))[X.loc[:, new_col].astype(int)]\n",
    "                else:\n",
    "                    X.loc[:, new_col] = categories.categories[X.loc[:, new_col].astype(int)]\n",
    "        else:\n",
    "            if self.add_na:\n",
    "                X = pd.Series(np.array(['#na#'] + list(self.categories[0].categories))[X], name=X.name, index=X.index)\n",
    "            else:\n",
    "                X = pd.Series(self.categories[0].categories[X], name=X.name, index=X.index)\n",
    "        return X\n",
    "\n",
    "    def to_categorical(self, categories):\n",
    "        if is_listy(categories[0]):\n",
    "            return [pd.CategoricalDtype(categories=np.sort(c) if self.sort else c, ordered=True) for c in categories]\n",
    "        else:\n",
    "            return pd.CategoricalDtype(categories=np.sort(categories) if self.sort else categories, ordered=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Stateful transforms like TSCategoricalEncoder can easily be serialized. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import joblib"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   b  B\n",
       "1   b  A\n",
       "2   b  C\n",
       "3   a  C\n",
       "4   b  C\n",
       ".. .. ..\n",
       "95  a  A\n",
       "96  a  A\n",
       "97  a  B\n",
       "98  a  A\n",
       "99  b  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>1</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>2</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   0  2\n",
       "1   0  1\n",
       "2   2  3\n",
       "3   1  3\n",
       "4   2  3\n",
       ".. .. ..\n",
       "95  1  1\n",
       "96  1  1\n",
       "97  1  2\n",
       "98  1  1\n",
       "99  2  2\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>#na#</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "       a  b\n",
       "0   #na#  B\n",
       "1   #na#  A\n",
       "2      b  C\n",
       "3      a  C\n",
       "4      b  C\n",
       "..   ... ..\n",
       "95     a  A\n",
       "96     a  A\n",
       "97     a  B\n",
       "98     a  A\n",
       "99     b  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = alphabet[np.random.randint(0,2,100)]\n",
    "df[\"b\"] = ALPHABET[np.random.randint(0,3,100)]\n",
    "display(df)\n",
    "a_unique = len(df[\"a\"].unique())\n",
    "b_unique = len(df[\"b\"].unique())\n",
    "tfm = TSCategoricalEncoder()\n",
    "tfm.fit(df, idxs=slice(0, 50))\n",
    "joblib.dump(tfm, \"data/TSCategoricalEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSCategoricalEncoder.joblib\")\n",
    "df.loc[0, \"a\"] = 'z'\n",
    "df.loc[1, \"a\"] = 'h'\n",
    "df = tfm.transform(df)\n",
    "display(df)\n",
    "test_eq(df['a'].max(), a_unique)\n",
    "test_eq(df['b'].max(), b_unique)\n",
    "df = tfm.inverse_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>b</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>b</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   b  B\n",
       "1   a  C\n",
       "2   b  C\n",
       "3   a  C\n",
       "4   b  B\n",
       ".. .. ..\n",
       "95  b  A\n",
       "96  b  A\n",
       "97  a  A\n",
       "98  b  B\n",
       "99  b  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>d</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>c</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>c</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>d</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>c</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>e</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   d  A\n",
       "1   a  A\n",
       "2   c  A\n",
       "3   a  A\n",
       "4   a  B\n",
       ".. .. ..\n",
       "95  c  C\n",
       "96  d  B\n",
       "97  c  A\n",
       "98  b  B\n",
       "99  e  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>1</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>0</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>2</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>0</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    a  b\n",
       "0   0  1\n",
       "1   1  1\n",
       "2   0  1\n",
       "3   1  1\n",
       "4   1  2\n",
       ".. .. ..\n",
       "95  0  3\n",
       "96  0  2\n",
       "97  0  1\n",
       "98  2  2\n",
       "99  0  2\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>a</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>a</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>#na#</td>\n",
       "      <td>C</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>#na#</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>#na#</td>\n",
       "      <td>A</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>b</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>#na#</td>\n",
       "      <td>B</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 2 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "       a  b\n",
       "0   #na#  A\n",
       "1      a  A\n",
       "2   #na#  A\n",
       "3      a  A\n",
       "4      a  B\n",
       "..   ... ..\n",
       "95  #na#  C\n",
       "96  #na#  B\n",
       "97  #na#  A\n",
       "98     b  B\n",
       "99  #na#  B\n",
       "\n",
       "[100 rows x 2 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = alphabet[np.random.randint(0,2,100)]\n",
    "df[\"a\"] = df[\"a\"].astype('category')\n",
    "df[\"b\"] = ALPHABET[np.random.randint(0,3,100)]\n",
    "display(df)\n",
    "a_unique = len(df[\"a\"].unique())\n",
    "b_unique = len(df[\"b\"].unique())\n",
    "tfm = TSCategoricalEncoder()\n",
    "tfm.fit(df)\n",
    "joblib.dump(tfm, \"data/TSCategoricalEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSCategoricalEncoder.joblib\")\n",
    "df[\"a\"] = alphabet[np.random.randint(0,5,100)]\n",
    "df[\"a\"] = df[\"a\"].astype('category')\n",
    "df[\"b\"] = ALPHABET[np.random.randint(0,3,100)]\n",
    "display(df)\n",
    "df = tfm.transform(df)\n",
    "display(df)\n",
    "test_eq(df['a'].max(), a_unique)\n",
    "test_eq(df['b'].max(), b_unique)\n",
    "df = tfm.inverse_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0     a\n",
       "1     b\n",
       "2     a\n",
       "3     a\n",
       "4     a\n",
       "     ..\n",
       "95    a\n",
       "96    a\n",
       "97    a\n",
       "98    a\n",
       "99    b\n",
       "Name: a, Length: 100, dtype: category\n",
       "Categories (2, object): ['a', 'b']"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "0     1\n",
       "1     2\n",
       "2     1\n",
       "3     1\n",
       "4     1\n",
       "     ..\n",
       "95    1\n",
       "96    1\n",
       "97    1\n",
       "98    1\n",
       "99    2\n",
       "Length: 100, dtype: int8"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "0     a\n",
       "1     b\n",
       "2     a\n",
       "3     a\n",
       "4     a\n",
       "     ..\n",
       "95    a\n",
       "96    a\n",
       "97    a\n",
       "98    a\n",
       "99    b\n",
       "Length: 100, dtype: object"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = alphabet[np.random.randint(0,2,100)]\n",
    "df[\"a\"] = df[\"a\"].astype('category')\n",
    "s = df['a']\n",
    "display(s)\n",
    "tfm = TSCategoricalEncoder()\n",
    "tfm.fit(s)\n",
    "joblib.dump(tfm, \"data/TSCategoricalEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSCategoricalEncoder.joblib\")\n",
    "s = tfm.transform(s)\n",
    "display(s)\n",
    "s = tfm.inverse_transform(s)\n",
    "display(s)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSTargetEncoder(TransformerMixin, BaseEstimator):\n",
    "    def __init__(self, \n",
    "        target_column, # column containing the target \n",
    "        columns=None, # List[str], optional. Columns to encode, all non-numerical columns by default.\n",
    "        inplace=True, # bool, optional. Modify input DataFrame, True by default.\n",
    "        prefix=None, # str, optional. Prefix for created column names. None by default.\n",
    "        suffix=None, # str, optional. Suffix for created column names. None by default.\n",
    "        drop=True, # bool, optional. Drop original columns, False by default.\n",
    "        dtypes=[\"object\", \"category\"], # List[str]. List with dtypes that will be used to identify columns to encode if not explicitly passed.\n",
    "        ):\n",
    "        \"Transforms categorical columns into numerical by replacing categories with target means.\"\n",
    "        \n",
    "        self.columns = listify(columns)\n",
    "        self.target_column = target_column\n",
    "        self.target_means = {}\n",
    "        self.inplace = inplace\n",
    "        self.prefix = prefix\n",
    "        self.suffix = suffix\n",
    "        self.drop = drop\n",
    "        self.dtypes = listify(dtypes)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns:\n",
    "            self.columns = X.select_dtypes(include=self.dtypes).columns\n",
    "                \n",
    "        assert self.target_column in X.columns\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        X_fit = X.loc[idxs]\n",
    "\n",
    "        for column in self.columns:\n",
    "            assert column in X.columns\n",
    "            self.target_means[column] = X_fit.groupby(column)[self.target_column].mean()\n",
    "\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.inplace:\n",
    "            X = X.copy()\n",
    "        for column in self.columns:\n",
    "            if column not in X.columns:\n",
    "                continue\n",
    "            name = []\n",
    "            if self.prefix: name += [self.prefix]\n",
    "            name += [column]\n",
    "            if self.suffix: name += [self.suffix]\n",
    "            new_col = '_'.join(name)\n",
    "            if self.drop:\n",
    "                X.loc[:, column] = X.loc[:, column].map(self.target_means[column])\n",
    "                X.rename(columns={column: new_col}, inplace=True)\n",
    "            else:\n",
    "                X.loc[:, new_col] = X.loc[:, column].map(self.target_means[column])\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        raise NotImplementedError(\"This method cannot be implemented because the original data cannot be reconstructed exactly.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>category1</th>\n",
       "      <th>category2</th>\n",
       "      <th>continuous</th>\n",
       "      <th>target</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.896091</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.318003</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.110052</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>large</td>\n",
       "      <td>0.227935</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.427108</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.325400</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.746491</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.649633</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.849223</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.657613</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 4 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "   category1 category2  continuous  target\n",
       "0     rabbit     small    0.896091       0\n",
       "1        cat     small    0.318003       1\n",
       "2     rabbit     small    0.110052       1\n",
       "3     rabbit     large    0.227935       0\n",
       "4        cat     large    0.427108       0\n",
       "..       ...       ...         ...     ...\n",
       "95       cat     small    0.325400       0\n",
       "96       cat     large    0.746491       0\n",
       "97    rabbit     small    0.649633       1\n",
       "98       cat     small    0.849223       0\n",
       "99       cat     large    0.657613       1\n",
       "\n",
       "[100 rows x 4 columns]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(80,)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>category1</th>\n",
       "      <th>category2</th>\n",
       "      <th>continuous</th>\n",
       "      <th>target</th>\n",
       "      <th>category1_te</th>\n",
       "      <th>category2_te</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.896091</td>\n",
       "      <td>0</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.318003</td>\n",
       "      <td>1</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.110052</td>\n",
       "      <td>1</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>large</td>\n",
       "      <td>0.227935</td>\n",
       "      <td>0</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.427108</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>...</th>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "      <td>...</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>95</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.325400</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>96</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.746491</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>97</th>\n",
       "      <td>rabbit</td>\n",
       "      <td>small</td>\n",
       "      <td>0.649633</td>\n",
       "      <td>1</td>\n",
       "      <td>0.565217</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>98</th>\n",
       "      <td>cat</td>\n",
       "      <td>small</td>\n",
       "      <td>0.849223</td>\n",
       "      <td>0</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.500000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>99</th>\n",
       "      <td>cat</td>\n",
       "      <td>large</td>\n",
       "      <td>0.657613</td>\n",
       "      <td>1</td>\n",
       "      <td>0.555556</td>\n",
       "      <td>0.521739</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>100 rows × 6 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "   category1 category2  continuous  target  category1_te  category2_te\n",
       "0     rabbit     small    0.896091       0      0.565217      0.500000\n",
       "1        cat     small    0.318003       1      0.555556      0.500000\n",
       "2     rabbit     small    0.110052       1      0.565217      0.500000\n",
       "3     rabbit     large    0.227935       0      0.565217      0.521739\n",
       "4        cat     large    0.427108       0      0.555556      0.521739\n",
       "..       ...       ...         ...     ...           ...           ...\n",
       "95       cat     small    0.325400       0      0.555556      0.500000\n",
       "96       cat     large    0.746491       0      0.555556      0.521739\n",
       "97    rabbit     small    0.649633       1      0.565217      0.500000\n",
       "98       cat     small    0.849223       0      0.555556      0.500000\n",
       "99       cat     large    0.657613       1      0.555556      0.521739\n",
       "\n",
       "[100 rows x 6 columns]"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# Create a dataframe with 100 rows\n",
    "np.random.seed(42)\n",
    "df = pd.DataFrame({\n",
    "    'category1': np.random.choice(['cat', 'dog', 'rabbit'], 100),\n",
    "    'category2': np.random.choice(['large', 'small'], 100),\n",
    "    'continuous': np.random.rand(100),\n",
    "    'target': np.random.randint(0, 2, 100)\n",
    "})\n",
    "\n",
    "display(df)\n",
    "\n",
    "# Split the data into train and test sets\n",
    "train_idx, test_idx = train_test_split(np.arange(100), test_size=0.2, random_state=42)\n",
    "print(train_idx.shape)\n",
    "\n",
    "# Initialize the encoder\n",
    "encoder = TSTargetEncoder(columns=['category1', 'category2'], target_column='target', inplace=False, suffix=\"te\", drop=False)\n",
    "\n",
    "# Fit the encoder using the training data\n",
    "encoder.fit(df, idxs=train_idx)\n",
    "\n",
    "# Transform the whole dataframe\n",
    "df_encoded = encoder.transform(df)\n",
    "\n",
    "# Check the results\n",
    "for c in [\"category1\", \"category2\"]:\n",
    "    for v in df[c].unique():\n",
    "        assert df.loc[train_idx][df.loc[train_idx, c] == v][\"target\"].mean() == df_encoded[df_encoded[c] == v][f\"{c}_te\"].mean()\n",
    "        \n",
    "df_encoded"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "default_date_attr = ['Year', 'Month', 'Week', 'Day', 'Dayofweek', 'Dayofyear', 'Is_month_end', 'Is_month_start', \n",
    "                     'Is_quarter_end', 'Is_quarter_start', 'Is_year_end', 'Is_year_start']\n",
    "\n",
    "class TSDateTimeEncoder(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, datetime_columns=None, prefix=None, drop=True, time=False, attr=default_date_attr):\n",
    "        self.datetime_columns = listify(datetime_columns)\n",
    "        self.prefix, self.drop, self.time, self.attr = prefix, drop, time, listify(attr)\n",
    "        \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if self.time: self.attr = self.attr + ['Hour', 'Minute', 'Second']\n",
    "        if not self.datetime_columns:\n",
    "            self.datetime_columns = X.columns\n",
    "        self.prefixes = []\n",
    "        for dt_column in self.datetime_columns: \n",
    "            self.prefixes.append(re.sub('[Dd]ate$', '', dt_column) if self.prefix is None else self.prefix)\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        \n",
    "        for dt_column,prefix in zip(self.datetime_columns,self.prefixes): \n",
    "            make_date(X, dt_column)\n",
    "            field = X[dt_column]\n",
    "\n",
    "            # Pandas removed `dt.week` in v1.1.10\n",
    "            week = field.dt.isocalendar().week.astype(field.dt.day.dtype) if hasattr(field.dt, 'isocalendar') else field.dt.week\n",
    "            for n in self.attr: X[prefix + \"_\" + n] = getattr(field.dt, n.lower()) if n != 'Week' else week\n",
    "            if self.drop: X = X.drop(self.datetime_columns, axis=1)\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime as dt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>_Year</th>\n",
       "      <th>_Month</th>\n",
       "      <th>_Week</th>\n",
       "      <th>_Day</th>\n",
       "      <th>_Dayofweek</th>\n",
       "      <th>_Dayofyear</th>\n",
       "      <th>_Is_month_end</th>\n",
       "      <th>_Is_month_start</th>\n",
       "      <th>_Is_quarter_end</th>\n",
       "      <th>_Is_quarter_start</th>\n",
       "      <th>_Is_year_end</th>\n",
       "      <th>_Is_year_start</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2023</td>\n",
       "      <td>6</td>\n",
       "      <td>24</td>\n",
       "      <td>17</td>\n",
       "      <td>5</td>\n",
       "      <td>168</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2023</td>\n",
       "      <td>6</td>\n",
       "      <td>24</td>\n",
       "      <td>18</td>\n",
       "      <td>6</td>\n",
       "      <td>169</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   _Year  _Month  _Week  _Day  _Dayofweek  _Dayofyear  _Is_month_end   \n",
       "0   2023       6     24    17           5         168          False  \\\n",
       "1   2023       6     24    18           6         169          False   \n",
       "\n",
       "   _Is_month_start  _Is_quarter_end  _Is_quarter_start  _Is_year_end   \n",
       "0            False            False              False         False  \\\n",
       "1            False            False              False         False   \n",
       "\n",
       "   _Is_year_start  \n",
       "0           False  \n",
       "1           False  "
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df.loc[0, \"date\"] = dt.datetime.now()\n",
    "df.loc[1, \"date\"] = dt.datetime.now() + pd.Timedelta(1, unit=\"D\")\n",
    "tfm = TSDateTimeEncoder()\n",
    "joblib.dump(tfm, \"data/TSDateTimeEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSDateTimeEncoder.joblib\")\n",
    "tfm.fit_transform(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSDropIfTrueCols(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, columns=None):\n",
    "        self.columns = listify(columns)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        mask = X[self.columns].sum(axis=1) == 0\n",
    "        X = X[mask].reset_index(drop=True)\n",
    "        X.drop(columns=self.columns, inplace=True)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        raise NotImplementedError(\"Inverse transform is not implemented for TSDropIfTrueCols\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(None,)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# test TSDropIfTrueCols\n",
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 0, 1, 0, 0]\n",
    "df[\"b\"] = [0, 0, 0, 0, 0]\n",
    "df[\"c\"] = [0, 1, 0, 0, 1]\n",
    "\n",
    "expected_output = pd.DataFrame()\n",
    "expected_output[\"b\"] = [0, 0, 0, 0]\n",
    "expected_output[\"c\"] = [0, 1, 0, 1]\n",
    "\n",
    "tfm = TSDropIfTrueCols(\"a\")\n",
    "output = tfm.fit_transform(df)\n",
    "test_eq(output, expected_output),\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSApplyFunction(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, function, groups=None, group_keys=False, axis=1, columns=None, reset_index=False, drop=True):\n",
    "        self.function = function\n",
    "        self.groups = listify(groups)\n",
    "        self.group_keys = group_keys\n",
    "        self.columns = listify(columns)\n",
    "        self.reset_index = reset_index\n",
    "        self.drop = drop\n",
    "        self.axis = axis\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if self.columns is None:\n",
    "            self.columns = X.columns\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if self.groups:\n",
    "            if self.columns:\n",
    "                X = X.groupby(self.groups, group_keys=self.group_keys)[self.columns].apply(lambda x: self.function(x))\n",
    "            else:\n",
    "                X = X.groupby(self.groups, group_keys=self.group_keys).apply(lambda x: self.function(x))\n",
    "        else:\n",
    "            if self.columns:\n",
    "                X[self.columns] = X[self.columns].apply(lambda x: self.function(x), axis=self.axis)\n",
    "            else:\n",
    "                X = X.apply(lambda x: self.function(x), axis=self.axis)\n",
    "        if self.reset_index: \n",
    "            X = X.reset_index(drop=self.drop)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        raise NotImplementedError(\"Inverse transform is not implemented for ApplyFunction\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "a    1\n",
       "b    1\n",
       "c    1\n",
       "dtype: int64"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 0, 1, 0, 0]\n",
    "df[\"b\"] = [0, 0, 0, 0, 0]\n",
    "df[\"c\"] = [0, 1, 0, 0, 1]\n",
    "\n",
    "df.apply(lambda x: 1, )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# test ApplyFunction without groups\n",
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 0, 1, 0, 0]\n",
    "df[\"b\"] = [0, 0, 0, 0, 0]\n",
    "df[\"c\"] = [0, 1, 0, 0, 1]\n",
    "\n",
    "expected_output = pd.Series([1,1,1])\n",
    "\n",
    "tfm = TSApplyFunction(lambda x: 1, axis=0, reset_index=True)\n",
    "output = tfm.fit_transform(df)\n",
    "test_eq(output, expected_output)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# test ApplyFunction with groups and square function\n",
    "df = pd.DataFrame()\n",
    "df[\"a\"] = [0, 1, 2, 3, 4]\n",
    "df[\"id\"] = [0, 0, 0, 1, 1]\n",
    "\n",
    "expected_output = pd.Series([5, 25])\n",
    "\n",
    "tfm = TSApplyFunction(lambda x: (x[\"a\"]**2).sum(), groups=\"id\")\n",
    "\n",
    "output = tfm.fit_transform(df)\n",
    "test_eq(output, expected_output)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSMissingnessEncoder(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, columns=None):\n",
    "        self.columns = listify(columns)\n",
    "        \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        self.missing_columns = [f\"{cn}_missing\" for cn in self.columns]\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        X[self.missing_columns] = X[self.columns].isnull().astype(int)\n",
    "        return X\n",
    "         \n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        X.drop(self.missing_columns, axis=1, inplace=True)\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>a_missing</th>\n",
       "      <th>b_missing</th>\n",
       "      <th>c_missing</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.511342</td>\n",
       "      <td>0.501516</td>\n",
       "      <td>0.798295</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.649964</td>\n",
       "      <td>0.701967</td>\n",
       "      <td>0.795793</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.337995</td>\n",
       "      <td>0.375583</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.093982</td>\n",
       "      <td>0.578280</td>\n",
       "      <td>0.035942</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.465598</td>\n",
       "      <td>0.542645</td>\n",
       "      <td>0.286541</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.590833</td>\n",
       "      <td>0.030500</td>\n",
       "      <td>0.037348</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.360191</td>\n",
       "      <td>0.127061</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.522243</td>\n",
       "      <td>0.769994</td>\n",
       "      <td>0.215821</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.622890</td>\n",
       "      <td>0.085347</td>\n",
       "      <td>0.051682</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c  a_missing  b_missing  c_missing\n",
       "0       NaN       NaN       NaN          1          1          1\n",
       "1  0.511342  0.501516  0.798295          0          0          0\n",
       "2  0.649964  0.701967  0.795793          0          0          0\n",
       "3       NaN  0.337995  0.375583          1          0          0\n",
       "4  0.093982  0.578280  0.035942          0          0          0\n",
       "5  0.465598  0.542645  0.286541          0          0          0\n",
       "6  0.590833  0.030500  0.037348          0          0          0\n",
       "7       NaN  0.360191  0.127061          1          0          0\n",
       "8  0.522243  0.769994  0.215821          0          0          0\n",
       "9  0.622890  0.085347  0.051682          0          0          0"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "data = np.random.rand(10,3)\n",
    "data[data > .8] = np.nan\n",
    "df = pd.DataFrame(data, columns=[\"a\", \"b\", \"c\"])\n",
    "tfm = TSMissingnessEncoder()\n",
    "tfm.fit(df)\n",
    "joblib.dump(tfm, \"data/TSMissingnessEncoder.joblib\")\n",
    "tfm = joblib.load(\"data/TSMissingnessEncoder.joblib\")\n",
    "df = tfm.transform(df)\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSSortByColumns(TransformerMixin, BaseEstimator):\n",
    "    \"Transforms a dataframe by sorting by columns.\"\n",
    "\n",
    "    def __init__(self, \n",
    "        columns, # Columns to sort by\n",
    "        ascending=True, # Ascending or descending\n",
    "        inplace=True, # Perform operation in place\n",
    "        kind='stable', # Type of sort to use\n",
    "        na_position='last', # Where to place NaNs\n",
    "        ignore_index=False, # Do not preserve index\n",
    "        key=None, # Function to apply to values before sorting\n",
    "        ):\n",
    "        self.columns, self.ascending, self.inplace, self.kind, self.na_position, self.ignore_index, self.key = \\\n",
    "        listify(columns), ascending, inplace, kind, na_position, ignore_index, key\n",
    "    \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.inplace:\n",
    "            X.sort_values(self.columns, axis=0, ascending=self.ascending, inplace=True, kind=self.kind, \n",
    "                          na_position=self.na_position, ignore_index=self.ignore_index, key=self.key)\n",
    "        else:\n",
    "            X = X.sort_values(self.columns, axis=0, ascending=self.ascending, inplace=False, kind=self.kind, \n",
    "                              na_position=self.na_position, ignore_index=self.ignore_index, key=self.key)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df_ori = df.copy()\n",
    "tfm = TSSortByColumns([\"a\", \"b\"])\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df_ori.sort_values([\"a\", \"b\"]).values, df.values)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSSelectColumns(TransformerMixin, BaseEstimator):\n",
    "    \"Transform used to select columns\"\n",
    "\n",
    "    def __init__(self, \n",
    "        columns # str or List[str]. Selected columns.\n",
    "        ):\n",
    "        self.columns = listify(columns)\n",
    "    \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, idxs=None, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if idxs is not None:\n",
    "            return X.loc[idxs, self.columns]\n",
    "        return X[self.columns].copy()\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df_ori = df.copy()\n",
    "tfm = TSSelectColumns([\"a\", \"b\"])\n",
    "df = tfm.fit_transform(df)\n",
    "test_eq(df_ori[[\"a\", \"b\"]].values, df.values)\n",
    "df = tfm.inverse_transform(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "PD_TIME_UNITS = dict([\n",
    "    (\"Y\", \"year\"), \n",
    "    (\"M\", \"month\"), \n",
    "    (\"W\", \"week\"), \n",
    "    (\"D\", \"day\"), \n",
    "    (\"h\", \"hour\"), \n",
    "    (\"m\", \"minute\"), \n",
    "    (\"s\", \"second\"), \n",
    "    (\"ms\", \"millisecond\"), \n",
    "    (\"us\", \"microsecond\"), \n",
    "    (\"ns\", \"nanosecond\"), \n",
    "    (\"ps\", \"picosecond\"), \n",
    "    (\"fs\", \"femtosecond\"), \n",
    "    (\"as\", \"attosecond\")\n",
    "])\n",
    "\n",
    "class TSStepsSinceStart(BaseEstimator, TransformerMixin):\n",
    "    \"Add a column indicating the number of steps since the start in each row\"\n",
    "\n",
    "    def __init__(self,\n",
    "        datetime_col, # (str or List[str]): Column name(s) containing datetime values.\n",
    "        datetime_unit=\"D\", #(str, optional): Time unit of the datetime values. Defaults to 'D'.\n",
    "        start_datetime=None, #(str or pd.Timestamp, optional): The start datetime value. If None, the minimum value of the datetime_col is used. Defaults to None.\n",
    "        drop=False, # (bool, optional): Whether to drop the datetime_col column after computing time steps. Defaults to False.\n",
    "        dtype=None, # (type, optional): Data type of the time steps. Defaults to None.\n",
    "):\n",
    "\n",
    "        self.datetime_col = listify(datetime_col)[0]\n",
    "        self.datetime_div = np.timedelta64(1, datetime_unit)\n",
    "        datetime_value = PD_TIME_UNITS[datetime_unit]\n",
    "        self.drop = drop\n",
    "        self.dtype = dtype\n",
    "        self.new_col = f\"{datetime_value}s_since_start\"\n",
    "        self.datetime_unit = datetime_unit\n",
    "        if start_datetime is None:\n",
    "            self.start_datetime = None  \n",
    "        elif isinstance(start_datetime, Timestamp):\n",
    "            self.start_datetime = start_datetime\n",
    "        else:\n",
    "            self.start_datetime = Timestamp(start_datetime)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.start_datetime is None:\n",
    "            self.start_datetime = X[self.datetime_col].min()\n",
    "        self.ori_dtype = X[self.datetime_col].dtypes\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        time_deltas = (pd.to_timedelta(X[self.datetime_col] - self.start_datetime) / self.datetime_div).astype(dtype=self.dtype)\n",
    "        if self.drop: \n",
    "            X[self.datetime_col] = time_deltas\n",
    "            X.rename(columns={self.datetime_col:self.new_col}, inplace=True)\n",
    "        else:\n",
    "            X[self.new_col] = time_deltas\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        datetimes  = pd.to_datetime(X[self.new_col] * self.datetime_div + self.start_datetime).astype(dtype=self.ori_dtype)\n",
    "        if self.drop: \n",
    "            X[self.new_col] = datetimes\n",
    "            X.rename(columns={self.new_col:self.datetime_col}, inplace=True)\n",
    "        else:\n",
    "            X[self.datetime_col] = datetimes\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.643288</td>\n",
       "      <td>0.458253</td>\n",
       "      <td>0.545617</td>\n",
       "      <td>2020-01-01</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.941465</td>\n",
       "      <td>0.386103</td>\n",
       "      <td>0.961191</td>\n",
       "      <td>2020-01-02</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.905351</td>\n",
       "      <td>0.195791</td>\n",
       "      <td>0.069361</td>\n",
       "      <td>2020-01-03</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.100778</td>\n",
       "      <td>0.018222</td>\n",
       "      <td>0.094443</td>\n",
       "      <td>2020-01-04</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.683007</td>\n",
       "      <td>0.071189</td>\n",
       "      <td>0.318976</td>\n",
       "      <td>2020-01-05</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.844875</td>\n",
       "      <td>0.023272</td>\n",
       "      <td>0.814468</td>\n",
       "      <td>2020-01-06</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.281855</td>\n",
       "      <td>0.118165</td>\n",
       "      <td>0.696737</td>\n",
       "      <td>2020-01-07</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.628943</td>\n",
       "      <td>0.877472</td>\n",
       "      <td>0.735071</td>\n",
       "      <td>2020-01-08</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.803481</td>\n",
       "      <td>0.282035</td>\n",
       "      <td>0.177440</td>\n",
       "      <td>2020-01-09</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.750615</td>\n",
       "      <td>0.806835</td>\n",
       "      <td>0.990505</td>\n",
       "      <td>2020-01-10</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime\n",
       "0  0.643288  0.458253  0.545617 2020-01-01\n",
       "1  0.941465  0.386103  0.961191 2020-01-02\n",
       "2  0.905351  0.195791  0.069361 2020-01-03\n",
       "3  0.100778  0.018222  0.094443 2020-01-04\n",
       "4  0.683007  0.071189  0.318976 2020-01-05\n",
       "5  0.844875  0.023272  0.814468 2020-01-06\n",
       "6  0.281855  0.118165  0.696737 2020-01-07\n",
       "7  0.628943  0.877472  0.735071 2020-01-08\n",
       "8  0.803481  0.282035  0.177440 2020-01-09\n",
       "9  0.750615  0.806835  0.990505 2020-01-10"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>days_since_start</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.643288</td>\n",
       "      <td>0.458253</td>\n",
       "      <td>0.545617</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.941465</td>\n",
       "      <td>0.386103</td>\n",
       "      <td>0.961191</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.905351</td>\n",
       "      <td>0.195791</td>\n",
       "      <td>0.069361</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.100778</td>\n",
       "      <td>0.018222</td>\n",
       "      <td>0.094443</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.683007</td>\n",
       "      <td>0.071189</td>\n",
       "      <td>0.318976</td>\n",
       "      <td>4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.844875</td>\n",
       "      <td>0.023272</td>\n",
       "      <td>0.814468</td>\n",
       "      <td>5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.281855</td>\n",
       "      <td>0.118165</td>\n",
       "      <td>0.696737</td>\n",
       "      <td>6</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.628943</td>\n",
       "      <td>0.877472</td>\n",
       "      <td>0.735071</td>\n",
       "      <td>7</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.803481</td>\n",
       "      <td>0.282035</td>\n",
       "      <td>0.177440</td>\n",
       "      <td>8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.750615</td>\n",
       "      <td>0.806835</td>\n",
       "      <td>0.990505</td>\n",
       "      <td>9</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c  days_since_start\n",
       "0  0.643288  0.458253  0.545617                 0\n",
       "1  0.941465  0.386103  0.961191                 1\n",
       "2  0.905351  0.195791  0.069361                 2\n",
       "3  0.100778  0.018222  0.094443                 3\n",
       "4  0.683007  0.071189  0.318976                 4\n",
       "5  0.844875  0.023272  0.814468                 5\n",
       "6  0.281855  0.118165  0.696737                 6\n",
       "7  0.628943  0.877472  0.735071                 7\n",
       "8  0.803481  0.282035  0.177440                 8\n",
       "9  0.750615  0.806835  0.990505                 9"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=10)\n",
    "display(df)\n",
    "df_ori = df.copy()\n",
    "tfm = TSStepsSinceStart(\"datetime\", datetime_unit=\"D\", drop=True, dtype=np.int32)\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)\n",
    "test_eq(df[\"days_since_start\"].values, np.arange(10))\n",
    "df = tfm.inverse_transform(df)\n",
    "test_eq(df_ori.values, df.values)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSStandardScaler(TransformerMixin, BaseEstimator):\n",
    "    \"Scale the values of specified columns in the input DataFrame to have a mean of 0 and standard deviation of 1.\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, # Column name(s) to be transformed. If None, all columns are transformed. Defaults to None.\n",
    "        mean=None, # Mean value for each column. If None, the mean value of each column is calculated during the fit method. Defaults to None.\n",
    "        std=None, # Stdev value for each column. If None, the standard deviation value of each column is calculated during the fit method. Defaults to None.\n",
    "        eps=1e-6, # A small value to avoid division by zero. Defaults to 1e-6.\n",
    "    ):\n",
    "        self.columns = listify(columns)\n",
    "        self.mean = mean\n",
    "        self.std = std\n",
    "        self.eps = np.array(eps, dtype='float32')\n",
    "    \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        if self.mean is None:\n",
    "            self.mean = []\n",
    "            for c in self.columns:\n",
    "                self.mean.append(X.loc[idxs, c].mean())\n",
    "        else:\n",
    "            assert len(self.mean) == len(self.columns)\n",
    "        if self.std is None:\n",
    "            self.std = []\n",
    "            for c in self.columns:\n",
    "                self.std.append(X.loc[idxs, c].std())\n",
    "        else:\n",
    "            assert len(self.std) == len(self.columns)\n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, s in zip(self.columns, self.mean, self.std):\n",
    "            X[c] = (X[c] - m) / (s + self.eps)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, s in zip(self.columns, self.mean, self.std):\n",
    "            X[c] = X[c] * (s + self.eps)  + m\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(100,3), columns=[\"a\", \"b\", \"c\"])\n",
    "tfm = TSStandardScaler()\n",
    "df = tfm.fit_transform(df)\n",
    "test_close(df.mean().values, np.zeros(3), 1e-3)\n",
    "test_close(df.std().values, np.ones(3), 1e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(1000,3), columns=[\"a\", \"b\", \"c\"])\n",
    "tfm = TSStandardScaler()\n",
    "df = tfm.fit_transform(df, idxs=slice(0, 800))\n",
    "test_close(df.mean().values, np.zeros(3), 1e-1)\n",
    "test_close(df.std().values, np.ones(3), 1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class TSRobustScaler(TransformerMixin, BaseEstimator):\n",
    "    \"\"\"This Scaler removes the median and scales the data according to the quantile range (defaults to IQR: Interquartile Range)\"\"\"\n",
    "\n",
    "    def __init__(self, columns=None, quantile_range=(25.0, 75.0), eps=1e-6):\n",
    "        self.columns = listify(columns)\n",
    "        self.quantile_range = quantile_range\n",
    "        self.eps = np.array(eps, dtype='float32')\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        idxs = fit_params.get(\"idxs\", slice(None))\n",
    "        \n",
    "        self.median = []\n",
    "        for c in self.columns:\n",
    "            self.median.append(np.nanpercentile(X.loc[idxs, c], 50))\n",
    "\n",
    "        self.iqr = []\n",
    "        for c in self.columns:\n",
    "            q1 = np.nanpercentile(X.loc[idxs, c], self.quantile_range[0])\n",
    "            q3 = np.nanpercentile(X.loc[idxs, c], self.quantile_range[1])\n",
    "            self.iqr.append(q3 - q1)\n",
    "                \n",
    "        return self\n",
    "\n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, q in zip(self.columns, self.median, self.iqr):\n",
    "            X[c] = (X[c] - m) / (q + self.eps)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        for c, m, q in zip(self.columns, self.median, self.iqr):\n",
    "            X[c] = X[c] * (q + self.eps) + m\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# test RobustScaler\n",
    "df = pd.DataFrame(np.random.rand(100,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"a\"] = df[\"a\"] * 100\n",
    "df[\"b\"] = df[\"b\"] * 10\n",
    "tfm = TSRobustScaler()\n",
    "df = tfm.fit_transform(df)\n",
    "test_close(df.median().values, np.zeros(3), 1e-3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSAddMissingTimestamps(TransformerMixin, BaseEstimator):\n",
    "    def __init__(self, datetime_col=None, use_index=False, unique_id_cols=None, fill_value=np.nan, range_by_group=True, \n",
    "                 start_date=None, end_date=None, freq=None):\n",
    "        assert datetime_col is not None or use_index\n",
    "        store_attr()\n",
    "        self.func = partial(add_missing_timestamps, datetime_col=datetime_col, use_index=use_index, unique_id_cols=unique_id_cols, \n",
    "                            fill_value=fill_value, range_by_group=range_by_group, start_date=start_date, end_date=end_date, \n",
    "                            freq=freq)\n",
    "    \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        X = self.func(X)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.211126</td>\n",
       "      <td>0.752468</td>\n",
       "      <td>0.051294</td>\n",
       "      <td>2020-01-01</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.394572</td>\n",
       "      <td>0.529941</td>\n",
       "      <td>0.161367</td>\n",
       "      <td>2020-01-03</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.571996</td>\n",
       "      <td>0.805432</td>\n",
       "      <td>0.760161</td>\n",
       "      <td>2020-01-04</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.361075</td>\n",
       "      <td>0.408456</td>\n",
       "      <td>0.679697</td>\n",
       "      <td>2020-01-06</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.056680</td>\n",
       "      <td>0.034673</td>\n",
       "      <td>0.391911</td>\n",
       "      <td>2020-01-07</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.259828</td>\n",
       "      <td>0.886086</td>\n",
       "      <td>0.895690</td>\n",
       "      <td>2020-01-09</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.297287</td>\n",
       "      <td>0.229994</td>\n",
       "      <td>0.411304</td>\n",
       "      <td>2020-01-10</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime\n",
       "0  0.211126  0.752468  0.051294 2020-01-01\n",
       "2  0.394572  0.529941  0.161367 2020-01-03\n",
       "3  0.571996  0.805432  0.760161 2020-01-04\n",
       "5  0.361075  0.408456  0.679697 2020-01-06\n",
       "6  0.056680  0.034673  0.391911 2020-01-07\n",
       "8  0.259828  0.886086  0.895690 2020-01-09\n",
       "9  0.297287  0.229994  0.411304 2020-01-10"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>datetime</th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0.211126</td>\n",
       "      <td>0.752468</td>\n",
       "      <td>0.051294</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2020-01-02</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0.394572</td>\n",
       "      <td>0.529941</td>\n",
       "      <td>0.161367</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0.571996</td>\n",
       "      <td>0.805432</td>\n",
       "      <td>0.760161</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2020-01-05</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0.361075</td>\n",
       "      <td>0.408456</td>\n",
       "      <td>0.679697</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0.056680</td>\n",
       "      <td>0.034673</td>\n",
       "      <td>0.391911</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2020-01-08</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0.259828</td>\n",
       "      <td>0.886086</td>\n",
       "      <td>0.895690</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>0.297287</td>\n",
       "      <td>0.229994</td>\n",
       "      <td>0.411304</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    datetime         a         b         c\n",
       "0 2020-01-01  0.211126  0.752468  0.051294\n",
       "1 2020-01-02       NaN       NaN       NaN\n",
       "2 2020-01-03  0.394572  0.529941  0.161367\n",
       "3 2020-01-04  0.571996  0.805432  0.760161\n",
       "4 2020-01-05       NaN       NaN       NaN\n",
       "5 2020-01-06  0.361075  0.408456  0.679697\n",
       "6 2020-01-07  0.056680  0.034673  0.391911\n",
       "7 2020-01-08       NaN       NaN       NaN\n",
       "8 2020-01-09  0.259828  0.886086  0.895690\n",
       "9 2020-01-10  0.297287  0.229994  0.411304"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=10)\n",
    "df = df.iloc[[0, 2, 3, 5, 6, 8, 9]]\n",
    "display(df)\n",
    "tfm = TSAddMissingTimestamps(datetime_col=\"datetime\", freq=\"D\")\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)\n",
    "test_eq(df.shape[0], 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        date  id  feature1  feature2\n",
       "0 2021-05-03   0  0.826065  0.793818\n",
       "1 2021-05-05   0  0.824350  0.577807\n",
       "2 2021-05-06   0  0.396992  0.866102\n",
       "3 2021-05-07   0  0.156317  0.289440\n",
       "4 2021-05-01   1  0.737951  0.467681\n",
       "5 2021-05-03   1  0.671271  0.411190\n",
       "6 2021-05-04   1  0.270644  0.427486\n",
       "7 2021-05-06   1  0.992582  0.564232"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-02</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         date  id  feature1  feature2\n",
       "0  2021-05-03   0  0.826065  0.793818\n",
       "1  2021-05-04   0       NaN       NaN\n",
       "2  2021-05-05   0  0.824350  0.577807\n",
       "3  2021-05-06   0  0.396992  0.866102\n",
       "4  2021-05-07   0  0.156317  0.289440\n",
       "5  2021-05-01   1  0.737951  0.467681\n",
       "6  2021-05-02   1       NaN       NaN\n",
       "7  2021-05-03   1  0.671271  0.411190\n",
       "8  2021-05-04   1  0.270644  0.427486\n",
       "9  2021-05-05   1       NaN       NaN\n",
       "10 2021-05-06   1  0.992582  0.564232"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "# Filling dates between min and max dates for each value in groupby column\n",
    "dates = pd.date_range('2021-05-01', '2021-05-07').values\n",
    "dates = np.concatenate((dates, dates))\n",
    "data = np.zeros((len(dates), 4))\n",
    "data[:, 0] = dates\n",
    "data[:, 1] = np.array([0]*(len(dates)//2)+[1]*(len(dates)//2))\n",
    "data[:, 2] = np.random.rand(len(dates))\n",
    "data[:, 3] = np.random.rand(len(dates))\n",
    "cols = ['date', 'id', 'feature1', 'feature2']\n",
    "date_df = pd.DataFrame(data, columns=cols).astype({'date': 'datetime64[ns]', 'id': int, 'feature1': float, 'feature2': float})\n",
    "date_df_with_missing_dates = date_df.drop([0,1,3,8,11,13]).reset_index(drop=True)\n",
    "display(date_df_with_missing_dates)\n",
    "tfm = TSAddMissingTimestamps(datetime_col=\"date\", unique_id_cols=\"id\", freq=\"D\")\n",
    "df = tfm.fit_transform(date_df_with_missing_dates.copy())\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        date  id  feature1  feature2\n",
       "0 2021-05-03   0  0.826065  0.793818\n",
       "1 2021-05-05   0  0.824350  0.577807\n",
       "2 2021-05-06   0  0.396992  0.866102\n",
       "3 2021-05-07   0  0.156317  0.289440\n",
       "4 2021-05-01   1  0.737951  0.467681\n",
       "5 2021-05-03   1  0.671271  0.411190\n",
       "6 2021-05-04   1  0.270644  0.427486\n",
       "7 2021-05-06   1  0.992582  0.564232"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>date</th>\n",
       "      <th>id</th>\n",
       "      <th>feature1</th>\n",
       "      <th>feature2</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2021-05-02</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0.826065</td>\n",
       "      <td>0.793818</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>0</td>\n",
       "      <td>0.824350</td>\n",
       "      <td>0.577807</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0.396992</td>\n",
       "      <td>0.866102</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>0</td>\n",
       "      <td>0.156317</td>\n",
       "      <td>0.289440</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2021-05-01</td>\n",
       "      <td>1</td>\n",
       "      <td>0.737951</td>\n",
       "      <td>0.467681</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>2021-05-02</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>2021-05-03</td>\n",
       "      <td>1</td>\n",
       "      <td>0.671271</td>\n",
       "      <td>0.411190</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>2021-05-04</td>\n",
       "      <td>1</td>\n",
       "      <td>0.270644</td>\n",
       "      <td>0.427486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>2021-05-05</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>2021-05-06</td>\n",
       "      <td>1</td>\n",
       "      <td>0.992582</td>\n",
       "      <td>0.564232</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>2021-05-07</td>\n",
       "      <td>1</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         date  id  feature1  feature2\n",
       "0  2021-05-01   0       NaN       NaN\n",
       "1  2021-05-02   0       NaN       NaN\n",
       "2  2021-05-03   0  0.826065  0.793818\n",
       "3  2021-05-04   0       NaN       NaN\n",
       "4  2021-05-05   0  0.824350  0.577807\n",
       "5  2021-05-06   0  0.396992  0.866102\n",
       "6  2021-05-07   0  0.156317  0.289440\n",
       "7  2021-05-01   1  0.737951  0.467681\n",
       "8  2021-05-02   1       NaN       NaN\n",
       "9  2021-05-03   1  0.671271  0.411190\n",
       "10 2021-05-04   1  0.270644  0.427486\n",
       "11 2021-05-05   1       NaN       NaN\n",
       "12 2021-05-06   1  0.992582  0.564232\n",
       "13 2021-05-07   1       NaN       NaN"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "display(date_df_with_missing_dates)\n",
    "tfm = TSAddMissingTimestamps(datetime_col=\"date\", unique_id_cols=\"id\", freq=\"D\", range_by_group=False)\n",
    "df = tfm.fit_transform(date_df_with_missing_dates.copy())\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSDropDuplicates(TransformerMixin, BaseEstimator):\n",
    "    \"Drop rows with duplicated values in a set of columns, optionally including a datetime column or index\"\n",
    "\n",
    "    def __init__(self,\n",
    "        datetime_col=None, #(str or List[str], optional): Name(s) of column(s) containing datetime values. If None, the index is used if use_index=True.\n",
    "        use_index=False, #(bool, optional): Whether to include the index in the set of columns for checking duplicates. Defaults to False.\n",
    "        unique_id_cols=None, #(str or List[str], optional): Name(s) of column(s) to be included in the set of columns for checking duplicates. Defaults to None.\n",
    "        keep='last', #(str, optional): Which duplicated values to keep. Choose from {'first', 'last', False}. Defaults to 'last'.\n",
    "        reset_index=False, #(bool, optional): Whether to reset the index after dropping duplicates. Ignored if use_index=False. Defaults to False.\n",
    "    ):\n",
    "        assert datetime_col is not None or use_index, \"you need to either pass a datetime_col or set use_index=True\"\n",
    "    \n",
    "        self.datetime_col, self.use_index, self.unique_id_cols, self.keep, self.reset_index =  \\\n",
    "        listify(datetime_col), use_index, listify(unique_id_cols), keep, reset_index\n",
    "    \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.use_index:\n",
    "            cols = [X.index.name or 'index'] + self.unique_id_cols\n",
    "            idxs_to_drop = X.reset_index().duplicated(subset=cols, keep=self.keep)\n",
    "        else:\n",
    "            cols = self.datetime_col + self.unique_id_cols\n",
    "            idxs_to_drop = X.duplicated(subset=cols, keep=self.keep)\n",
    "        if idxs_to_drop.sum():\n",
    "            X.drop(index=idxs_to_drop[idxs_to_drop].index, inplace=True)\n",
    "            if not self.use_index and self.reset_index:\n",
    "                X.reset_index(drop=True, inplace=True)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.201528</td>\n",
       "      <td>0.934433</td>\n",
       "      <td>0.689088</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.016200</td>\n",
       "      <td>0.818380</td>\n",
       "      <td>0.040139</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.016200</td>\n",
       "      <td>0.818380</td>\n",
       "      <td>0.040139</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.889913</td>\n",
       "      <td>0.991963</td>\n",
       "      <td>0.294067</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.865562</td>\n",
       "      <td>0.102843</td>\n",
       "      <td>0.125955</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.979152</td>\n",
       "      <td>0.673839</td>\n",
       "      <td>0.846887</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.979152</td>\n",
       "      <td>0.673839</td>\n",
       "      <td>0.846887</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.603150</td>\n",
       "      <td>0.682532</td>\n",
       "      <td>0.575359</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.429062</td>\n",
       "      <td>0.275923</td>\n",
       "      <td>0.768581</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime  user_id\n",
       "0  0.201528  0.934433  0.689088 2020-01-01        0\n",
       "1  0.016200  0.818380  0.040139 2020-01-03        0\n",
       "2  0.016200  0.818380  0.040139 2020-01-03        0\n",
       "3  0.889913  0.991963  0.294067 2020-01-04        0\n",
       "4  0.865562  0.102843  0.125955 2020-01-06        1\n",
       "5  0.979152  0.673839  0.846887 2020-01-07        1\n",
       "6  0.979152  0.673839  0.846887 2020-01-07        1\n",
       "7  0.603150  0.682532  0.575359 2020-01-09        1\n",
       "8  0.429062  0.275923  0.768581 2020-01-10        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.201528</td>\n",
       "      <td>0.934433</td>\n",
       "      <td>0.689088</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.016200</td>\n",
       "      <td>0.818380</td>\n",
       "      <td>0.040139</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.889913</td>\n",
       "      <td>0.991963</td>\n",
       "      <td>0.294067</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.865562</td>\n",
       "      <td>0.102843</td>\n",
       "      <td>0.125955</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.979152</td>\n",
       "      <td>0.673839</td>\n",
       "      <td>0.846887</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.603150</td>\n",
       "      <td>0.682532</td>\n",
       "      <td>0.575359</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.429062</td>\n",
       "      <td>0.275923</td>\n",
       "      <td>0.768581</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          a         b         c   datetime  user_id\n",
       "0  0.201528  0.934433  0.689088 2020-01-01        0\n",
       "2  0.016200  0.818380  0.040139 2020-01-03        0\n",
       "3  0.889913  0.991963  0.294067 2020-01-04        0\n",
       "4  0.865562  0.102843  0.125955 2020-01-06        1\n",
       "6  0.979152  0.673839  0.846887 2020-01-07        1\n",
       "7  0.603150  0.682532  0.575359 2020-01-09        1\n",
       "8  0.429062  0.275923  0.768581 2020-01-10        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(10,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=10)\n",
    "df['user_id'] = np.sort(np.random.randint(0, 2, 10))\n",
    "df = df.iloc[[0, 2, 2, 3, 5, 6, 6, 8, 9]]\n",
    "df.reset_index(drop=True, inplace=True)\n",
    "display(df)\n",
    "tfm = TSDropDuplicates(datetime_col=\"datetime\", unique_id_cols=\"a\")\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSFillMissing(TransformerMixin, BaseEstimator):\n",
    "    \"Fill missing values in specified columns using the specified method and/ or value.\"\n",
    "\n",
    "    def __init__(self,\n",
    "        columns=None, #(str or List[str], optional): Column name(s) to be transformed. If None, all columns are transformed. Defaults to None.\n",
    "        unique_id_cols=None, #(str or List[str], optional): Col name(s) with unique ids for each row. If None, uses all rows at once. Defaults to None .\n",
    "        method='ffill', #(str, optional): The method to use for filling missing values, e.g. 'ffill', 'bfill'. If None, `value` is used. Defaults to None.\n",
    "        value=0, #(scalar or dict or Series, optional): The value to use for filling missing values. If None, `method` is used. Defaults to None.\n",
    "    ):\n",
    "\n",
    "        self.columns = listify(columns)\n",
    "        self.unique_id_cols = unique_id_cols\n",
    "        self.method = method\n",
    "        self.value = value\n",
    "    \n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if not self.columns:\n",
    "            if isinstance(X, pd.DataFrame):\n",
    "                self.columns = X.columns\n",
    "            else:\n",
    "                self.columns = X.name\n",
    "        return self\n",
    "        \n",
    "    def transform(self, X, **kwargs):\n",
    "        assert isinstance(X, (pd.DataFrame, pd.Series))\n",
    "        if self.method is not None:\n",
    "            for c in self.columns:\n",
    "                if self.unique_id_cols is not None:\n",
    "                    X[c] = X.groupby(self.unique_id_cols)[c].fillna(method=self.method)\n",
    "                else:\n",
    "                    X[c] = X[c].fillna(method=self.method)\n",
    "        if self.value is not None:\n",
    "            for c in self.columns:\n",
    "                X[c] = X[c].fillna(value=self.value)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X, **kwargs):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.059943</td>\n",
       "      <td>0.130974</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.928860</td>\n",
       "      <td>0.331972</td>\n",
       "      <td>0.465337</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.631375</td>\n",
       "      <td>0.426398</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.576881</td>\n",
       "      <td>0.563920</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.500279</td>\n",
       "      <td>0.069394</td>\n",
       "      <td>0.089877</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.600912</td>\n",
       "      <td>0.340959</td>\n",
       "      <td>0.917268</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.143281</td>\n",
       "      <td>0.714719</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.525470</td>\n",
       "      <td>0.697833</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.792191</td>\n",
       "      <td>0.676361</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.945925</td>\n",
       "      <td>0.295824</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.271955</td>\n",
       "      <td>0.217891</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.633712</td>\n",
       "      <td>0.593461</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.016243</td>\n",
       "      <td>0.728778</td>\n",
       "      <td>0.323530</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.556578</td>\n",
       "      <td>0.342731</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>0.134576</td>\n",
       "      <td>0.094419</td>\n",
       "      <td>0.831518</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id\n",
       "0        NaN  0.059943  0.130974 2020-01-01        0\n",
       "1   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "2   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "3   0.928860  0.331972  0.465337 2020-01-04        0\n",
       "4        NaN  0.631375  0.426398 2020-01-06        0\n",
       "5   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "6   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "7        NaN  0.576881  0.563920 2020-01-09        0\n",
       "8   0.500279  0.069394  0.089877 2020-01-10        0\n",
       "9   0.600912  0.340959  0.917268 2020-01-11        0\n",
       "10  0.406591  0.143281  0.714719 2020-01-12        0\n",
       "11       NaN  0.525470  0.697833 2020-01-13        1\n",
       "12       NaN  0.792191  0.676361 2020-01-14        1\n",
       "13       NaN  0.945925  0.295824 2020-01-15        1\n",
       "14       NaN  0.271955  0.217891 2020-01-16        1\n",
       "15       NaN  0.633712  0.593461 2020-01-17        1\n",
       "16  0.016243  0.728778  0.323530 2020-01-18        1\n",
       "17       NaN  0.556578  0.342731 2020-01-19        1\n",
       "18  0.134576  0.094419  0.831518 2020-01-20        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.059943</td>\n",
       "      <td>0.130974</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.734151</td>\n",
       "      <td>0.341319</td>\n",
       "      <td>0.478528</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>0.928860</td>\n",
       "      <td>0.331972</td>\n",
       "      <td>0.465337</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.928860</td>\n",
       "      <td>0.631375</td>\n",
       "      <td>0.426398</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.174647</td>\n",
       "      <td>0.295932</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>0.548145</td>\n",
       "      <td>0.576881</td>\n",
       "      <td>0.563920</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.500279</td>\n",
       "      <td>0.069394</td>\n",
       "      <td>0.089877</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.600912</td>\n",
       "      <td>0.340959</td>\n",
       "      <td>0.917268</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.143281</td>\n",
       "      <td>0.714719</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.525470</td>\n",
       "      <td>0.697833</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.792191</td>\n",
       "      <td>0.676361</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.945925</td>\n",
       "      <td>0.295824</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.271955</td>\n",
       "      <td>0.217891</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>0.406591</td>\n",
       "      <td>0.633712</td>\n",
       "      <td>0.593461</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.016243</td>\n",
       "      <td>0.728778</td>\n",
       "      <td>0.323530</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>0.016243</td>\n",
       "      <td>0.556578</td>\n",
       "      <td>0.342731</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>0.134576</td>\n",
       "      <td>0.094419</td>\n",
       "      <td>0.831518</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id\n",
       "0   0.000000  0.059943  0.130974 2020-01-01        0\n",
       "1   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "2   0.734151  0.341319  0.478528 2020-01-03        0\n",
       "3   0.928860  0.331972  0.465337 2020-01-04        0\n",
       "4   0.928860  0.631375  0.426398 2020-01-06        0\n",
       "5   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "6   0.548145  0.174647  0.295932 2020-01-07        0\n",
       "7   0.548145  0.576881  0.563920 2020-01-09        0\n",
       "8   0.500279  0.069394  0.089877 2020-01-10        0\n",
       "9   0.600912  0.340959  0.917268 2020-01-11        0\n",
       "10  0.406591  0.143281  0.714719 2020-01-12        0\n",
       "11  0.406591  0.525470  0.697833 2020-01-13        1\n",
       "12  0.406591  0.792191  0.676361 2020-01-14        1\n",
       "13  0.406591  0.945925  0.295824 2020-01-15        1\n",
       "14  0.406591  0.271955  0.217891 2020-01-16        1\n",
       "15  0.406591  0.633712  0.593461 2020-01-17        1\n",
       "16  0.016243  0.728778  0.323530 2020-01-18        1\n",
       "17  0.016243  0.556578  0.342731 2020-01-19        1\n",
       "18  0.134576  0.094419  0.831518 2020-01-20        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(20,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df.loc[np.random.rand(20) > .5, 'a'] = np.nan\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=20)\n",
    "df['user_id'] = np.sort(np.random.randint(0, 2, 20))\n",
    "df = df.iloc[[0, 2, 2, 3, 5, 6, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]\n",
    "df.reset_index(drop=True, inplace=True)\n",
    "display(df)\n",
    "tfm = TSFillMissing(columns=\"a\", method=\"ffill\", value=0)\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)\n",
    "test_eq(df['a'].isna().sum(), 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class TSMissingnessEncoder(BaseEstimator, TransformerMixin):\n",
    "\n",
    "    def __init__(self, columns=None):\n",
    "        self.columns = listify(columns)\n",
    "\n",
    "    def fit(self, X, y=None, **fit_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        if not self.columns: self.columns = X.columns\n",
    "        self.missing_columns = [f\"{cn}_missing\" for cn in self.columns]\n",
    "        return self\n",
    "\n",
    "    def transform(self, X:pd.DataFrame, y=None, **transform_params):\n",
    "        assert isinstance(X, pd.DataFrame)\n",
    "        X[self.missing_columns] = X[self.columns].isnull().astype(np.int16)\n",
    "        return X\n",
    "\n",
    "    def inverse_transform(self, X):\n",
    "        return X"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.873619</td>\n",
       "      <td>0.995569</td>\n",
       "      <td>0.582714</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.133210</td>\n",
       "      <td>0.632396</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.700611</td>\n",
       "      <td>0.753472</td>\n",
       "      <td>0.872859</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.617106</td>\n",
       "      <td>0.849959</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.196246</td>\n",
       "      <td>0.125550</td>\n",
       "      <td>0.963480</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.108045</td>\n",
       "      <td>0.478491</td>\n",
       "      <td>0.585564</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.086032</td>\n",
       "      <td>0.057027</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>0.105483</td>\n",
       "      <td>0.585588</td>\n",
       "      <td>0.544345</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>0.233741</td>\n",
       "      <td>0.637774</td>\n",
       "      <td>0.820068</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.498130</td>\n",
       "      <td>0.689310</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.307771</td>\n",
       "      <td>0.613638</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>0.897935</td>\n",
       "      <td>0.809924</td>\n",
       "      <td>0.583130</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.730222</td>\n",
       "      <td>0.364822</td>\n",
       "      <td>0.640966</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>0.466182</td>\n",
       "      <td>0.189936</td>\n",
       "      <td>0.701738</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.358622</td>\n",
       "      <td>0.911339</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id\n",
       "0   0.873619  0.995569  0.582714 2020-01-01        0\n",
       "1   0.402704  0.672507  0.682192 2020-01-03        0\n",
       "2   0.402704  0.672507  0.682192 2020-01-03        0\n",
       "3        NaN  0.133210  0.632396 2020-01-04        0\n",
       "4   0.700611  0.753472  0.872859 2020-01-06        0\n",
       "5        NaN  0.730249  0.619173 2020-01-07        0\n",
       "6        NaN  0.730249  0.619173 2020-01-07        0\n",
       "7        NaN  0.617106  0.849959 2020-01-09        0\n",
       "8   0.196246  0.125550  0.963480 2020-01-10        1\n",
       "9   0.108045  0.478491  0.585564 2020-01-11        1\n",
       "10       NaN  0.086032  0.057027 2020-01-12        1\n",
       "11  0.105483  0.585588  0.544345 2020-01-13        1\n",
       "12  0.233741  0.637774  0.820068 2020-01-14        1\n",
       "13       NaN  0.498130  0.689310 2020-01-15        1\n",
       "14       NaN  0.307771  0.613638 2020-01-16        1\n",
       "15  0.897935  0.809924  0.583130 2020-01-17        1\n",
       "16  0.730222  0.364822  0.640966 2020-01-18        1\n",
       "17  0.466182  0.189936  0.701738 2020-01-19        1\n",
       "18       NaN  0.358622  0.911339 2020-01-20        1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>a</th>\n",
       "      <th>b</th>\n",
       "      <th>c</th>\n",
       "      <th>datetime</th>\n",
       "      <th>user_id</th>\n",
       "      <th>a_missing</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.873619</td>\n",
       "      <td>0.995569</td>\n",
       "      <td>0.582714</td>\n",
       "      <td>2020-01-01</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>0.402704</td>\n",
       "      <td>0.672507</td>\n",
       "      <td>0.682192</td>\n",
       "      <td>2020-01-03</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.133210</td>\n",
       "      <td>0.632396</td>\n",
       "      <td>2020-01-04</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>0.700611</td>\n",
       "      <td>0.753472</td>\n",
       "      <td>0.872859</td>\n",
       "      <td>2020-01-06</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.730249</td>\n",
       "      <td>0.619173</td>\n",
       "      <td>2020-01-07</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.617106</td>\n",
       "      <td>0.849959</td>\n",
       "      <td>2020-01-09</td>\n",
       "      <td>0</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>0.196246</td>\n",
       "      <td>0.125550</td>\n",
       "      <td>0.963480</td>\n",
       "      <td>2020-01-10</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>0.108045</td>\n",
       "      <td>0.478491</td>\n",
       "      <td>0.585564</td>\n",
       "      <td>2020-01-11</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.086032</td>\n",
       "      <td>0.057027</td>\n",
       "      <td>2020-01-12</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>0.105483</td>\n",
       "      <td>0.585588</td>\n",
       "      <td>0.544345</td>\n",
       "      <td>2020-01-13</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>0.233741</td>\n",
       "      <td>0.637774</td>\n",
       "      <td>0.820068</td>\n",
       "      <td>2020-01-14</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.498130</td>\n",
       "      <td>0.689310</td>\n",
       "      <td>2020-01-15</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.307771</td>\n",
       "      <td>0.613638</td>\n",
       "      <td>2020-01-16</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>0.897935</td>\n",
       "      <td>0.809924</td>\n",
       "      <td>0.583130</td>\n",
       "      <td>2020-01-17</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>0.730222</td>\n",
       "      <td>0.364822</td>\n",
       "      <td>0.640966</td>\n",
       "      <td>2020-01-18</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>0.466182</td>\n",
       "      <td>0.189936</td>\n",
       "      <td>0.701738</td>\n",
       "      <td>2020-01-19</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>NaN</td>\n",
       "      <td>0.358622</td>\n",
       "      <td>0.911339</td>\n",
       "      <td>2020-01-20</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           a         b         c   datetime  user_id  a_missing\n",
       "0   0.873619  0.995569  0.582714 2020-01-01        0          0\n",
       "1   0.402704  0.672507  0.682192 2020-01-03        0          0\n",
       "2   0.402704  0.672507  0.682192 2020-01-03        0          0\n",
       "3        NaN  0.133210  0.632396 2020-01-04        0          1\n",
       "4   0.700611  0.753472  0.872859 2020-01-06        0          0\n",
       "5        NaN  0.730249  0.619173 2020-01-07        0          1\n",
       "6        NaN  0.730249  0.619173 2020-01-07        0          1\n",
       "7        NaN  0.617106  0.849959 2020-01-09        0          1\n",
       "8   0.196246  0.125550  0.963480 2020-01-10        1          0\n",
       "9   0.108045  0.478491  0.585564 2020-01-11        1          0\n",
       "10       NaN  0.086032  0.057027 2020-01-12        1          1\n",
       "11  0.105483  0.585588  0.544345 2020-01-13        1          0\n",
       "12  0.233741  0.637774  0.820068 2020-01-14        1          0\n",
       "13       NaN  0.498130  0.689310 2020-01-15        1          1\n",
       "14       NaN  0.307771  0.613638 2020-01-16        1          1\n",
       "15  0.897935  0.809924  0.583130 2020-01-17        1          0\n",
       "16  0.730222  0.364822  0.640966 2020-01-18        1          0\n",
       "17  0.466182  0.189936  0.701738 2020-01-19        1          0\n",
       "18       NaN  0.358622  0.911339 2020-01-20        1          1"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Test\n",
    "df = pd.DataFrame(np.random.rand(20,3), columns=[\"a\", \"b\", \"c\"])\n",
    "df.loc[np.random.rand(20) > .5, 'a'] = np.nan\n",
    "df[\"datetime\"] = pd.date_range(\"2020-01-01\", periods=20)\n",
    "df['user_id'] = np.sort(np.random.randint(0, 2, 20))\n",
    "df = df.iloc[[0, 2, 2, 3, 5, 6, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]]\n",
    "df.reset_index(drop=True, inplace=True)\n",
    "display(df)\n",
    "tfm = TSMissingnessEncoder(columns=\"a\")\n",
    "df = tfm.fit_transform(df)\n",
    "display(df)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "With these sklearn preprocessing API transforms it's possible to build data preprocessing pipelines like this one:\n",
    "\n",
    "```python\n",
    "from sklearn.pipeline import Pipeline\n",
    "\n",
    "cont_cols = ['cont_0', 'cont_1', 'cont_2', 'cont_3', 'cont_4', 'cont_5']\n",
    "pipe = Pipeline([\n",
    "    ('shrinker', TSShrinkDataFrame()), \n",
    "    ('drop_duplicates', TSDropDuplicates('date', unique_id_cols='user_id')),\n",
    "    ('add_mts', TSAddMissingTimestamps(datetime_col='date', unique_id_cols='user_id', freq='D', range_by_group=False)),\n",
    "    ('onehot_encoder', TSOneHotEncoder(['cat_0'])),\n",
    "    ('cat_encoder', TSCategoricalEncoder(['user_id', 'cat_1'])),\n",
    "    ('steps_since_start', TSStepsSinceStart('date', datetime_unit='D', start_datetime='2017-01-01'), dtype=np.int32),\n",
    "    ('missing_encoder', TSMissingnessEncoder(['cont_1'])),\n",
    "    ('fill_missing', TSFillMissing(cont_cols, unique_id_cols='user_id', value=0)),\n",
    "    ], \n",
    "    verbose=True)\n",
    "df = pipe.fit_transform(df)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## y transforms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "class Preprocessor():\n",
    "    def __init__(self, preprocessor, **kwargs): \n",
    "        self.preprocessor = preprocessor(**kwargs)\n",
    "        \n",
    "    def fit(self, o): \n",
    "        if isinstance(o, pd.Series): o = o.values.reshape(-1,1)\n",
    "        else: o = o.reshape(-1,1)\n",
    "        self.fit_preprocessor = self.preprocessor.fit(o)\n",
    "        return self.fit_preprocessor\n",
    "    \n",
    "    def transform(self, o, copy=True):\n",
    "        if type(o) in [float, int]: o = array([o]).reshape(-1,1)\n",
    "        o_shape = o.shape\n",
    "        if isinstance(o, pd.Series): o = o.values.reshape(-1,1)\n",
    "        else: o = o.reshape(-1,1)\n",
    "        output = self.fit_preprocessor.transform(o).reshape(*o_shape)\n",
    "        if isinstance(o, torch.Tensor): return o.new(output)\n",
    "        return output\n",
    "    \n",
    "    def inverse_transform(self, o, copy=True):\n",
    "        o_shape = o.shape\n",
    "        if isinstance(o, pd.Series): o = o.values.reshape(-1,1)\n",
    "        else: o = o.reshape(-1,1)\n",
    "        output = self.fit_preprocessor.inverse_transform(o).reshape(*o_shape)\n",
    "        if isinstance(o, torch.Tensor): return o.new(output)\n",
    "        return output\n",
    "\n",
    "\n",
    "StandardScaler = partial(sklearn.preprocessing.StandardScaler)\n",
    "setattr(StandardScaler, '__name__', 'StandardScaler')\n",
    "RobustScaler = partial(sklearn.preprocessing.RobustScaler)\n",
    "setattr(RobustScaler, '__name__', 'RobustScaler')\n",
    "Normalizer = partial(sklearn.preprocessing.MinMaxScaler, feature_range=(-1, 1))\n",
    "setattr(Normalizer, '__name__', 'Normalizer')\n",
    "BoxCox = partial(sklearn.preprocessing.PowerTransformer, method='box-cox')\n",
    "setattr(BoxCox, '__name__', 'BoxCox')\n",
    "YeoJohnshon = partial(sklearn.preprocessing.PowerTransformer, method='yeo-johnson')\n",
    "setattr(YeoJohnshon, '__name__', 'YeoJohnshon')\n",
    "Quantile = partial(sklearn.preprocessing.QuantileTransformer, n_quantiles=1_000, output_distribution='normal', random_state=0)\n",
    "setattr(Quantile, '__name__', 'Quantile')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Standardize\n",
    "from tsai.data.validation import TimeSplitter"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAABZcAAABoCAYAAACNDM73AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAdz0lEQVR4nO3deVSV1f7H8c9hOkwCoghqAmY4JGrmQJSWLQcwrzexybIuWlcr57xa2a8QzRzL5XXWXFdbN0uzq2XmRJaYhfMspqYglJqmIuLEtH9/lCdPkngUDkHv11rPWufZz36e/d2nvgrftd2PxRhjBAAAAAAAAACAA1zKOgAAAAAAAAAAQPlDcRkAAAAAAAAA4DCKywAAAAAAAAAAh1FcBgAAAAAAAAA4jOIyAAAAAAAAAMBhFJcBAAAAAAAAAA6juAwAAAAAAAAAcBjFZQAAAAAAAACAwyguAwAAAAAAAAAcRnEZAACglMybN08Wi0Xp6em2tjZt2qhNmzYlPlZiYqIsFotdW3h4uHr06FHiY/1eenq6LBaL5s2bZ2vr0aOHfH19S33sKywWixITE502HgAAAACKywAAADa7d+/Wo48+qrCwMHl6eqpmzZpq3769pkyZUmpjHj16VImJidqxY0epjeGI5cuX/2mLtH/m2AAAAIC/IreyDgAAAODP4Ntvv9WDDz6o0NBQ9erVSyEhIcrMzNSGDRv073//W/379y+RcVavXm13fvToUY0YMULh4eG66667SmSMK/bv3y8XF8fWEixfvlzTpk1zqIgbFhamixcvyt3d3cEIHXO92C5evCg3N360BQAAAJyJn8ABAAAkvfXWW/L399fmzZsVEBBgd+3EiRMlNo6Hh0eJPas4Vqu1VJ+fn5+vwsJCeXh4yNPTs1THKk5Zjw8AAAD8FbEtBgAAgKRDhw6pYcOG1xSWJalatWp25xaLRf369dP8+fNVr149eXp6qlmzZlq3bl2x41y95/LatWvVokULSVLPnj1lsViu2bu4KOvXr1eLFi3k6empOnXqaNasWUX2+/2ey3l5eRoxYoQiIiLk6empKlWqqFWrVkpKSpL0yz7J06ZNs83xyiH9tq/y22+/rUmTJqlOnTqyWq1KTU0tcs/lKw4fPqyYmBj5+PioRo0aGjlypIwxtutr166VxWLR2rVr7e77/TOvF9uVtt+vaN6+fbs6duwoPz8/+fr6qm3bttqwYYNdnyv7Yn/zzTcaPHiwgoKC5OPjo7i4OJ08ebLo/wAAAAAAJLFyGQAAQNIvWzukpKRoz549ioyMLLZ/cnKyFi5cqAEDBshqtWr69OmKjY3Vpk2bbuh+SWrQoIFGjhyphIQE9e7dW61bt5Yk3XvvvX94z+7du9WhQwcFBQUpMTFR+fn5Gj58uIKDg4sdLzExUWPGjNE///lPtWzZUtnZ2dqyZYu2bdum9u3b6/nnn9fRo0eVlJSk//73v0U+Y+7cubp06ZJ69+4tq9WqwMBAFRYWFtm3oKBAsbGxuueeezR+/HitXLlSw4cPV35+vkaOHHkD39BvbiS2q+3du1etW7eWn5+fXn75Zbm7u2vWrFlq06aNkpOTFRUVZde/f//+qly5soYPH6709HRNmjRJ/fr108KFCx2KEwAAAPgrobgMAAAgaciQIerYsaPuuusutWzZUq1bt1bbtm314IMPFrmX8J49e7RlyxY1a9ZMktStWzfVq1dPCQkJWrx48Q2NGRwcrI4dOyohIUHR0dF6+umni70nISFBxhh9/fXXCg0NlSQ98sgjatSoUbH3fv7553rooYc0e/bsIq9HR0erbt26SkpK+sNYfvjhB33//fcKCgqytaWnpxfZ99KlS4qNjdXkyZMlSX369FHnzp01btw4DRgwQFWrVi02Zkdiu9rrr7+uvLw8rV+/Xrfffrsk6R//+Ifq1aunl19+WcnJyXb9q1SpotWrV9tWQxcWFmry5Mk6e/as/P39bzhOAAAA4K+EbTEAAAAktW/fXikpKfr73/+unTt3avz48YqJiVHNmjW1dOnSa/pHR0fbCsuSFBoaqocfflirVq1SQUFBqcRYUFCgVatWqUuXLrbCsvTLCuiYmJhi7w8ICNDevXt18ODBm47hkUcesSssF6dfv362z1e2E8nNzdUXX3xx0zEUp6CgQKtXr1aXLl1shWVJql69up566imtX79e2dnZdvf07t3bbpuN1q1bq6CgQEeOHCm1OAEAAIDyjuIyAADAr1q0aKHFixfrzJkz2rRpk4YNG6Zz587p0UcfVWpqql3fiIiIa+6vW7euLly4UGp79Z48eVIXL14scux69eoVe//IkSOVlZWlunXrqlGjRho6dKh27drlUAy1a9e+4b4uLi52xV3pl+9I+uPVziXh5MmTunDhQpHfSYMGDVRYWKjMzEy79quL9ZJUuXJlSdKZM2dKLU4AAACgvKO4DAAA8DseHh5q0aKFRo8erRkzZigvL0+LFi0q67Bu2f33369Dhw7pP//5jyIjIzVnzhzdfffdmjNnzg0/w8vLq0Rjunq18NVKa/X3H3F1dS2y/eqXDwIAAACwR3EZAADgOpo3by5JOnbsmF17UVtLHDhwQN7e3g5tG/FHxdWiBAUFycvLq8ix9+/ff0PPCAwMVM+ePfXhhx8qMzNTjRs3VmJi4k3FU5zCwkIdPnzYru3AgQOSpPDwcEm/rRDOysqy61fUdhQ3GltQUJC8vb2L/E6+++47ubi4qFatWjf0LAAAAAB/jOIyAACApK+++qrIVarLly+XdO22EykpKdq2bZvtPDMzU59++qk6dOjwh6tgi+Lj4yPp2uJqUVxdXRUTE6NPPvlEGRkZtvZ9+/Zp1apVxd5/6tQpu3NfX1/dcccdunz58k3FcyOmTp1q+2yM0dSpU+Xu7q62bdtKksLCwuTq6qp169bZ3Td9+vRrnnWjsbm6uqpDhw769NNP7bbf+Omnn/TBBx+oVatW8vPzu8kZAQAAALjCrawDAAAA+DPo37+/Lly4oLi4ONWvX1+5ubn69ttvtXDhQoWHh6tnz552/SMjIxUTE6MBAwbIarXaiqEjRoxwaNw6deooICBAM2fOVKVKleTj46OoqKg/3Nt4xIgRWrlypVq3bq0+ffooPz9fU6ZMUcOGDYvdP/nOO+9UmzZt1KxZMwUGBmrLli36+OOP7V66d+UlhQMGDFBMTIxcXV3VrVs3h+Z0haenp1auXKn4+HhFRUVpxYoV+vzzz/Xaa6/ZVnf7+/vrscce05QpU2SxWFSnTh0tW7ZMJ06cuOZ5jsQ2atQoJSUlqVWrVurTp4/c3Nw0a9YsXb58WePHj7+p+QAAAACwR3EZAABA0ttvv61FixZp+fLlmj17tnJzcxUaGqo+ffro9ddfV0BAgF3/Bx54QNHR0RoxYoQyMjJ05513at68eWrcuLFD47q7u+u9997TsGHD9MILLyg/P19z5879w+Jy48aNtWrVKg0ePFgJCQm67bbbNGLECB07dqzY4vKAAQO0dOlSrV69WpcvX1ZYWJhGjRqloUOH2vp07dpV/fv314IFC/T+++/LGHPTxWVXV1etXLlSL774ooYOHapKlSpp+PDhSkhIsOs3ZcoU5eXlaebMmbJarXr88cc1YcIERUZG2vVzJLaGDRvq66+/1rBhwzRmzBgVFhYqKipK77//vqKiom5qPgAAAADsWQxvKQEAAHCIxWJR37597bZ8AAAAAIC/GvZcBgAAAAAAAAA4jOIyAAAAAAAAAMBhFJcBAAAAAAAAAA7jhX4AAAAO4pUVAAAAAMDKZQAAAAAAAADATaC4DAAAAAAAAABwmNO3xSgsLNTRo0dVqVIlWSwWZw8PAAAAAAAAlGvGGJ07d041atSQiwtrR1F2nF5cPnr0qGrVquXsYQEAAAAAAIAKJTMzU7fddltZh4G/MKcXlytVqvTrp0xJfs4eHgAAAACAv4wmyfeXdQgASkHB+QLteWjPVXU2oGw4vbj821YYfqK4DAAAAABA6XH1dS3rEACUIracRVljUxYAAAAAAAAAgMMoLgMAAAAAAAAAHEZxGQAAAAAAAADgMKfvuQwAAAAAAAAApaGgoEB5eXllHUa55erqKjc3txvez5viMgAAAAAAAIByLycnRz/88IOMMWUdSrnm7e2t6tWry8PDo9i+FJcBAAAAAAAAlGsFBQX64Ycf5O3traCgoBteeYvfGGOUm5urkydPKi0tTREREXJxuf6uyhSXAQAAAAAAAJRreXl5MsYoKChIXl5eZR1OueXl5SV3d3cdOXJEubm58vT0vG5/XugHAAAAAAAAoEJgxfKtK261sl3fUowDAAAAAAAAAFBBUVwGAAAAAAAAADiM4jIAAAAAAAAAVBDh4eGaNGmSU8aiuAwAAAAAAACgQrJYnHs4FpvlukdiYuJNzXnz5s3q3bv3Td3rKIeLy+vWrVPnzp1Vo0YNWSwWffLJJ6UQFgAAAAAAAABUXMeOHbMdkyZNkp+fn13bkCFDbH2NMcrPz7+h5wYFBcnb27u0wrbjcHH5/PnzatKkiaZNm1Ya8QAAAAAAAABAhRcSEmI7/P39ZbFYbOffffedKlWqpBUrVqhZs2ayWq1av369Dh06pIcffljBwcHy9fVVixYt9MUXX9g99/fbYlgsFs2ZM0dxcXHy9vZWRESEli5dWiJzcLi43LFjR40aNUpxcXElEgAAAAAAAAAA4Fqvvvqqxo4dq3379qlx48bKycnRQw89pDVr1mj79u2KjY1V586dlZGRcd3njBgxQo8//rh27dqlhx56SN27d9fp06dvOb5S33P58uXLys7OtjsAAAAAAAAAANc3cuRItW/fXnXq1FFgYKCaNGmi559/XpGRkYqIiNCbb76pOnXqFLsSuUePHnryySd1xx13aPTo0crJydGmTZtuOb5SLy6PGTNG/v7+tqNWrVqlPSQAAAAAAAAAlHvNmze3O8/JydGQIUPUoEEDBQQEyNfXV/v27St25XLjxo1tn318fOTn56cTJ07ccnylXlweNmyYzp49azsyMzNLe0gAAAAAAAAAKPd8fHzszocMGaIlS5Zo9OjR+vrrr7Vjxw41atRIubm5132Ou7u73bnFYlFhYeEtx+d2y08ohtVqldVqLe1hAAAAAAAAAKBC++abb9SjRw/b+/BycnKUnp5eZvGU+splAAAAAAAAAMCti4iI0OLFi7Vjxw7t3LlTTz31VImsQL5ZDq9czsnJ0ffff287T0tL044dOxQYGKjQ0NASDQ4AAAAAAAAAbpYxZR1ByZo4caKeffZZ3XvvvapatapeeeUVZWdnl1k8FmMc+4rXrl2rBx988Jr2+Ph4zZs3r9j7s7Oz5e/vL+msJD9HhgYAAAAAAA64e2uzsg4BQCkoyCnQzgd26uzZs/Lzo74mSZcuXVJaWppq164tT0/Psg6nXHPku3R45XKbNm3kYD0aAAAAAAAAAFDBsOcyAAAAAAAAAMBhFJcBAAAAAAAAAA6juAwAAAAAAAAAcBjFZQAAAAAAAACAwyguAwAAAAAAAAAcRnEZAAAAAAAAAOAwissAAAAAAAAAAIdRXAYAAAAAAAAAOIziMgAAAAAAAADAYW5lHQAAAAAAAAAAlIZm25o5dbytd2+94b4Wi+W614cPH67ExMSbisNisWjJkiXq0qXLTd1/oyguAwAAAAAAAICTHTt2zPZ54cKFSkhI0P79+21tvr6+ZRGWQ5xeXDbG/Pop29lDAwAAAADwl1KQU1DWIQAoBQXnf8nt3+psKI9CQkJsn/39/WWxWOza5syZo3feeUdpaWkKDw/XgAED1KdPH0lSbm6uBg8erP/97386c+aMgoOD9cILL2jYsGEKDw+XJMXFxUmSwsLClJ6eXipzcHpx+dSpU79+quXsoQEAAAAA+EvZ+UBZRwCgNJ06dUr+/v5lHQZKwfz585WQkKCpU6eqadOm2r59u3r16iUfHx/Fx8dr8uTJWrp0qT766COFhoYqMzNTmZmZkqTNmzerWrVqmjt3rmJjY+Xq6lpqcTq9uBwYGChJysjI4H9+oILJzs5WrVq1lJmZKT8/v7IOB0AJIr+Biov8Biou8huouM6ePavQ0FBbnQ0Vz/Dhw/XOO++oa9eukqTatWsrNTVVs2bNUnx8vDIyMhQREaFWrVrJYrEoLCzMdm9QUJAkKSAgwG4ldGlwenHZxcVF0i9LvfnLDaiY/Pz8yG+ggiK/gYqL/AYqLvIbqLiu1NlQsZw/f16HDh3Sc889p169etna8/PzbYt1e/Toofbt26tevXqKjY3V3/72N3Xo0MHpsfJCPwAAAAAAAAD4k8jJyZEkvfvuu4qKirK7dmWLi7vvvltpaWlasWKFvvjiCz3++ONq166dPv74Y6fGSnEZAAAAAAAAAP4kgoODVaNGDR0+fFjdu3f/w35+fn564okn9MQTT+jRRx9VbGysTp8+rcDAQLm7u6ugoPRf6ur04rLVatXw4cNltVqdPTSAUkZ+AxUX+Q1UXOQ3UHGR30DFRX5XfCNGjNCAAQPk7++v2NhYXb58WVu2bNGZM2c0ePBgTZw4UdWrV1fTpk3l4uKiRYsWKSQkRAEBAZKk8PBwrVmzRvfdd5+sVqsqV65cKnFajDGmVJ4MAAAAAAAAAE5w6dIlpaWlqXbt2vL09CzrcBw2b948DRo0SFlZWba2Dz74QBMmTFBqaqp8fHzUqFEjDRo0SHFxcXr33Xc1ffp0HTx4UK6urmrRooUmTJigpk2bSpI+++wzDR48WOnp6apZs6bS09NvOBZHvkuKywAAAAAAAADKtfJeXP4zceS75JWSAAAAAAAAAACHUVwGAAAAAAAAADiM4jIAAAAAAAAAwGEUlwEAAAAAAAAADnNqcXnatGkKDw+Xp6enoqKitGnTJmcOD8BBY8aMUYsWLVSpUiVVq1ZNXbp00f79++36XLp0SX379lWVKlXk6+urRx55RD/99JNdn4yMDHXq1Ene3t6qVq2ahg4dqvz8fGdOBUAxxo4dK4vFokGDBtnayG+g/Prxxx/19NNPq0qVKvLy8lKjRo20ZcsW23VjjBISElS9enV5eXmpXbt2OnjwoN0zTp8+re7du8vPz08BAQF67rnnlJOT4+ypALhKQUGB3njjDdWuXVteXl6qU6eO3nzzTRljbH3Ib6D8WLdunTp37qwaNWrIYrHok08+sbteUvm8a9cutW7dWp6enqpVq5bGjx9f2lMrU1f/mYib48h36LTi8sKFCzV48GANHz5c27ZtU5MmTRQTE6MTJ044KwQADkpOTlbfvn21YcMGJSUlKS8vTx06dND58+dtfV566SV99tlnWrRokZKTk3X06FF17drVdr2goECdOnVSbm6uvv32W7333nuaN2+eEhISymJKAIqwefNmzZo1S40bN7ZrJ7+B8unMmTO677775O7urhUrVig1NVXvvPOOKleubOszfvx4TZ48WTNnztTGjRvl4+OjmJgYXbp0ydane/fu2rt3r5KSkrRs2TKtW7dOvXv3LospAfjVuHHjNGPGDE2dOlX79u3TuHHjNH78eE2ZMsXWh/wGyo/z58+rSZMmmjZtWpHXSyKfs7Oz1aFDB4WFhWnr1q2aMGGCEhMTNXv27FKfn7O5urpKknJzc8s4kvLvwoULkiR3d/fiOxsnadmypenbt6/tvKCgwNSoUcOMGTPGWSEAuEUnTpwwkkxycrIxxpisrCzj7u5uFi1aZOuzb98+I8mkpKQYY4xZvny5cXFxMcePH7f1mTFjhvHz8zOXL1927gQAXOPcuXMmIiLCJCUlmQceeMAMHDjQGEN+A+XZK6+8Ylq1avWH1wsLC01ISIiZMGGCrS0rK8tYrVbz4YcfGmOMSU1NNZLM5s2bbX1WrFhhLBaL+fHHH0sveADX1alTJ/Pss8/atXXt2tV0797dGEN+A+WZJLNkyRLbeUnl8/Tp003lypXtfj5/5ZVXTL169Up5Rs5XWFho0tPTzcGDB8358+fNxYsXORw8Lly4YH7++WeTmppqjh49ekPfu1tpVbivlpubq61bt2rYsGG2NhcXF7Vr104pKSnOCAFACTh79qwkKTAwUJK0detW5eXlqV27drY+9evXV2hoqFJSUnTPPfcoJSVFjRo1UnBwsK1PTEyMXnzxRe3du1dNmzZ17iQA2Onbt686deqkdu3aadSoUbZ28hsov5YuXaqYmBg99thjSk5OVs2aNdWnTx/16tVLkpSWlqbjx4/b5be/v7+ioqKUkpKibt26KSUlRQEBAWrevLmtT7t27eTi4qKNGzcqLi7O6fMCIN17772aPXu2Dhw4oLp162rnzp1av369Jk6cKIn8BiqSksrnlJQU3X///fLw8LD1iYmJ0bhx43TmzBm7f9lU3lksFlWvXl1paWk6cuRIWYdTrgUEBCgkJOSG+jqluPzzzz+roKDA7pdPSQoODtZ3333njBAA3KLCwkINGjRI9913nyIjIyVJx48fl4eHhwICAuz6BgcH6/jx47Y+ReX+lWsAys6CBQu0bds2bd68+Zpr5DdQfh0+fFgzZszQ4MGD9dprr2nz5s0aMGCAPDw8FB8fb8vPovL36vyuVq2a3XU3NzcFBgaS30AZevXVV5Wdna369evL1dVVBQUFeuutt9S9e3dJIr+BCqSk8vn48eOqXbv2Nc+4cq0iFZclycPDQxEREWyNcQvc3d1tW4zcCKcUlwGUf3379tWePXu0fv36sg4FQAnIzMzUwIEDlZSUJE9Pz7IOB0AJKiwsVPPmzTV69GhJUtOmTbVnzx7NnDlT8fHxZRwdgFvx0Ucfaf78+frggw/UsGFD7dixQ4MGDVKNGjXIbwD4lYuLC7/jOJFTXuhXtWpVubq6XvOG+Z9++umGl1gDKDv9+vXTsmXL9NVXX+m2226ztYeEhCg3N1dZWVl2/a/O7ZCQkCJz/8o1AGVj69atOnHihO6++265ubnJzc1NycnJmjx5stzc3BQcHEx+A+VU9erVdeedd9q1NWjQQBkZGZJ+y8/r/WweEhJyzYu38/Pzdfr0afIbKENDhw7Vq6++qm7duqlRo0Z65pln9NJLL2nMmDGSyG+gIimpfOZndpQ2pxSXPTw81KxZM61Zs8bWVlhYqDVr1ig6OtoZIQC4CcYY9evXT0uWLNGXX355zT+ladasmdzd3e1ye//+/crIyLDldnR0tHbv3m33F15SUpL8/Pyu+cUXgPO0bdtWu3fv1o4dO2xH8+bN1b17d9tn8hson+677z7t37/fru3AgQMKCwuTJNWuXVshISF2+Z2dna2NGzfa5XdWVpa2bt1q6/Pll1+qsLBQUVFRTpgFgKJcuHBBLi72v8a7urqqsLBQEvkNVCQllc/R0dFat26d8vLybH2SkpJUr169CrclBspI6b6n8TcLFiwwVqvVzJs3z6SmpprevXubgIAAuzfMA/hzefHFF42/v79Zu3atOXbsmO24cOGCrc8LL7xgQkNDzZdffmm2bNlioqOjTXR0tO16fn6+iYyMNB06dDA7duwwK1euNEFBQWbYsGFlMSUA1/HAAw+YgQMH2s7Jb6B82rRpk3FzczNvvfWWOXjwoJk/f77x9vY277//vq3P2LFjTUBAgPn000/Nrl27zMMPP2xq165tLl68aOsTGxtrmjZtajZu3GjWr19vIiIizJNPPlkWUwLwq/j4eFOzZk2zbNkyk5aWZhYvXmyqVq1qXn75ZVsf8hsoP86dO2e2b99utm/fbiSZiRMnmu3bt5sjR44YY0omn7OyskxwcLB55plnzJ49e8yCBQuMt7e3mTVrltPni4rJacVlY4yZMmWKCQ0NNR4eHqZly5Zmw4YNzhwegIMkFXnMnTvX1ufixYumT58+pnLlysbb29vExcWZY8eO2T0nPT3ddOzY0Xh5eZmqVauaf/3rXyYvL8/JswFQnN8Xl8lvoPz67LPPTGRkpLFaraZ+/fpm9uzZdtcLCwvNG2+8YYKDg43VajVt27Y1+/fvt+tz6tQp8+STTxpfX1/j5+dnevbsac6dO+fMaQD4nezsbDNw4EATGhpqPD09ze23327+7//+z1y+fNnWh/wGyo+vvvqqyN+54+PjjTEll887d+40rVq1Mlar1dSsWdOMHTvWWVPEX4DFGGPKZs00AAAAAAAAAKC8csqeywAAAAAAAACAioXiMgAAAAAAAADAYRSXAQAAAAAAAAAOo7gMAAAAAAAAAHAYxWUAAAAAAAAAgMMoLgMAAAAAAAAAHEZxGQAAAAAAAADgMIrLAAAAAAAAAACHUVwGAAAAAAAAADiM4jIAAAAAAAAAwGEUlwEAAAAAAAAADvt/jKsGon+cvpoAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "y = random_shuffle(np.random.randn(1000) * 10 + 5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(StandardScaler)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGdCAYAAACyzRGfAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAkfklEQVR4nO3df3BU1f3/8dcmZHcJYTcEIUtKAvFH+VGL2Ki4hXYUoymDDpTUqjgtWEYrjViIVkl/SHDUpNgKYgOopcH+kdIyHahowTqpxLEmFKP4s6ZiiQmGBLVNFkOzicn9/uGH/XYFNZvsnrubPB8zdyZ77s3dt2di8uLsOfc4LMuyBAAAYEiS3QUAAIDhhfABAACMInwAAACjCB8AAMAowgcAADCK8AEAAIwifAAAAKMIHwAAwKgRdhfwSX19fWppadHo0aPlcDjsLgcAAPSDZVk6fvy4srKylJT02WMbcRc+WlpalJ2dbXcZAABgAJqbmzVx4sTPvCbuwsfo0aMlfVy8x+OxuRoAANAfgUBA2dnZob/jnyXuwsfJj1o8Hg/hAwCABNOfKRNMOAUAAEYRPgAAgFGEDwAAYFTczfkAAMAulmXpo48+Um9vr92lxKWUlBQlJycP+j6EDwAAJHV3d+vo0aM6ceKE3aXELYfDoYkTJyotLW1Q9yF8AACGvb6+Ph0+fFjJycnKysqS0+nkQZefYFmW3nvvPR05ckTnnHPOoEZACB8AgGGvu7tbfX19ys7OVmpqqt3lxK1x48apsbFRPT09gwofTDgFAOD/fN5jwYe7aI0G0csAAMAowgcAADAq4jkf7777ru68807t2bNHJ06c0Nlnn63KykpdcMEFkj6ekLJmzRo9+uijam9v1+zZs7V582adc845US8eAIBYm7z6SaPv11g+38j7lJaWateuXTp48KCR9/tfEY18/Oc//9Hs2bOVkpKiPXv26I033tAvf/lLjRkzJnTNunXrtHHjRm3ZskX79+/XqFGjVFBQoK6urqgXDwAABub2229XdXW1Le8d0cjHz3/+c2VnZ6uysjLUlpubG/rasixt2LBBP/3pT7VgwQJJ0m9/+1tlZmZq165duvbaa6NUNgAAGAjLstTb26u0tLRBP69joCIa+Xj88cd1wQUX6Oqrr9b48eN1/vnn69FHHw2dP3z4sFpbW5Wfnx9q83q9mjVrlmpra097z2AwqEAgEHYAAID+CwaDuvXWWzV+/Hi53W7NmTNHBw4ckCTt27dPDodDe/bsUV5enlwul5577jmVlpZq5syZttQb0cjHv/71L23evFnFxcX68Y9/rAMHDujWW2+V0+nUkiVL1NraKknKzMwM+77MzMzQuU8qKyvT2rVrB1g+MMyVeqXSDrurCOnPZ+OmPs8GhpM77rhDf/zjH/XYY49p0qRJWrdunQoKCnTo0KHQNatXr9YvfvELnXnmmRozZoz27dtnW70RjXz09fXpK1/5iu677z6df/75uummm3TjjTdqy5YtAy6gpKREHR0doaO5uXnA9wIAYLjp7OzU5s2bdf/992vevHmaPn26Hn30UY0cOVJbt24NXXf33Xfr8ssv11lnnaWMjAwbK44wfEyYMEHTp08Pa5s2bZqampokST6fT5LU1tYWdk1bW1vo3Ce5XC55PJ6wAwAA9M/bb7+tnp4ezZ49O9SWkpKiiy66SP/4xz9CbSdXpcaDiMLH7Nmz1dDQENb2z3/+U5MmTZL08eRTn88XNns2EAho//798vv9USgXAAAMxKhRo+wuISSi8LFq1SrV1dXpvvvu06FDh1RVVaVHHnlERUVFkj5+7OrKlSt1zz336PHHH9err76q7373u8rKytLChQtjUT8AAMPaWWedJafTqb/97W+htp6eHh04cOCUTyviRUQTTi+88ELt3LlTJSUluvvuu5Wbm6sNGzbo+uuvD11zxx13qLOzUzfddJPa29s1Z84c7d27V263O+rFAwAw3I0aNUrLly/Xj370I2VkZCgnJ0fr1q3TiRMntGzZMr388st2l3iKiJ9weuWVV+rKK6/81PMOh0N333237r777kEVBuBzlHqjditWqQCfLhF+9svLy9XX16fvfOc7On78uC644AI99dRTYQ8BjSfs7QIAQIJzu93auHGj3nvvPXV1dem5557ThRdeKEm65JJLZFmW0tPTw76ntLTUlkerS4QPAABgGOEDAAAYRfgAAABGET4AAIBRhA8AAGAU4QMAABhF+AAAAEYRPgAAgFGEDwAAhpi//e1v+vKXv6yUlJS43Fst4serAwAwrERxK4P+vV9HRJdfcsklmjlzpjZs2BBqKy4u1syZM7Vnzx6lpaVFucDBY+QDAIAh5u2339bcuXM1ceLEUx6rHg8IHwAAJKilS5eqpqZG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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# RobustScaler\n",
    "y = random_shuffle(np.random.randn(1000) * 10 + 5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(RobustScaler)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Normalize\n",
    "y = random_shuffle(np.random.rand(1000) * 3 + .5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(Normalizer)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGdCAYAAACyzRGfAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAm6ElEQVR4nO3df3BU5b3H8c8Skg2/srkB8qtJIICCCIFe1BixFCESomOhoqK2FVoGCxO8xVTFdFR+tHdCaadiHRq991rSVpHqvQJTLWQgkjDagBDkolQZkgkCFxIsHZIQLklu9rl/WBbX/IBNdp/dTd6vmTNmz3n2Od89e3b5ePac5ziMMUYAAACW9At2AQAAoG8hfAAAAKsIHwAAwCrCBwAAsIrwAQAArCJ8AAAAqwgfAADAKsIHAACwqn+wC/gqt9ut06dPa8iQIXI4HMEuBwAAXANjjBobG5WcnKx+/bo+thFy4eP06dNKTU0NdhkAAKAbTp48qZSUlC7bhFz4GDJkiKQvio+JiQlyNQAA4Fo0NDQoNTXV8+94V0IufFz+qSUmJobwAQBAmLmWUyY44RQAAFhF+AAAAFYRPgAAgFUhd84HAADBYozR//3f/6mtrS3YpYSkyMhIRURE9LgfwgcAAJJaWlp05swZXbx4MdilhCyHw6GUlBQNHjy4R/0QPgAAfZ7b7VZNTY0iIiKUnJysqKgoBrr8CmOMPv/8c506dUrXXXddj46AED4AAH1eS0uL3G63UlNTNXDgwGCXE7KGDx+u48ePq7W1tUfhgxNOAQD4h6sNC97X+etoEFsZAABYRfgAAABWcc4HAABdGPn0O1bXd3zt3VbWs2rVKm3dulWHDh2ysr4v48gHAAB90BNPPKHS0tKgrJsjHwAA9CHGGLW1tWnw4ME9Hq+juzjyAQBAmGtubta//Mu/KD4+XtHR0br99tu1f/9+SVJZWZkcDoe2b9+uKVOmyOl06r333tOqVas0efLkoNRL+EDPrXJ9MQEAguKpp57Sf/3Xf+l3v/udDh48qDFjxignJ0d///vfPW2efvpprV27Vp988okyMjKCWC0/uwAAENaamppUVFSk4uJi5ebmSpL+/d//XTt37tQrr7yim2++WZK0Zs0a3XnnncEs1YMjHwAAhLHq6mq1trZq6tSpnnmRkZG65ZZb9Mknn3jm3XTTTcEor0OEDwAA+oBBgwYFuwQPwgcAAGFs9OjRioqK0vvvv++Z19raqv3792v8+PFBrKxznPMBAEAYGzRokJYuXaonn3xScXFxSktL07p163Tx4kUtWrRI//3f/x3sEtshfAAA0AVbI472xNq1a+V2u/W9731PjY2Nuummm1RSUqJ/+qd/CnZpHXIYY0ywi/iyhoYGuVwu1dfXKyYmJtjl4Fpcvsx2VX1w6wCAbrp06ZJqamqUnp6u6OjoYJcTsrraTr78+805HwAAwCrCBwAAsIrwAQAArCJ8AAAAqwgfAADAKsIHAACwivABAACs8il8FBUVKSMjQzExMYqJiVFWVpa2b9/uWT59+nQ5HA6vacmSJX4vGgAAhC+fwkdKSorWrl2ryspKHThwQDNmzNCcOXN05MgRT5vFixfrzJkznmndunV+LxoAAHTu/fff18SJExUZGam5c+cGu5x2fBpe/Z577vF6/K//+q8qKirS3r17deONN0qSBg4cqMTERP9VCABAMF0exdna+nwbLXr69OmaPHmy1q9f75mXn5+vyZMna/v27Ro8eLCfC+y5bp/z0dbWps2bN6upqUlZWVme+a+99pqGDRumCRMmqKCgQBcvXuyyn+bmZjU0NHhNAACg+6qrqzVjxgylpKQoNjY22OW043P4+OijjzR48GA5nU4tWbJEW7Zs8dyy9+GHH9arr76q3bt3q6CgQH/4wx/03e9+t8v+CgsL5XK5PFNqamr3XgkAAH3MwoULVV5erhdeeMHrfMtz587pBz/4gRwOh4qLi1VWViaHw6GSkhJ9/etf14ABAzRjxgydPXtW27dv1w033KCYmBg9/PDDVz1o4A8+31iupaVFJ06cUH19vf7zP/9T//Ef/6Hy8nJPAPmyd999VzNnzlRVVZVGjx7dYX/Nzc1qbm72PG5oaFBqaio3lgsn3FgOQJjr8sZyIfyzS319vXJzczVhwgStWbNGbW1tkqTx48drzZo1mj9/vlwul/bt26c77rhDt956q375y19q4MCBeuCBB/S1r31NTqdTa9eu1YULF/Ttb39bTz75pFasWNHh+vx1YzmfzvmQpKioKI0ZM0aSNGXKFO3fv18vvPCCXn755XZtMzMzJanL8OF0OuV0On0tAwCAPs/lcikqKqrd+ZYOh0Mul6vdOZg/+9nPNHXqVEnSokWLVFBQoOrqao0aNUqSdN99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bPPNb37T/OhHP7JTUIDccsstJi8vz/O4ra3NJCcnm8LCwg7bP/DAA+buu+/2mpeZmWl++MMfBrRO286ePWskmfLy8k7bbNy40bhcLntFWbJy5UozadKka27fV/YJY4z50Y9+ZEaPHm3cbneHy3vrPiHJbNmyxfPY7XabxMRE84tf/MIz7/z588bpdJrXX3+90358/b4JRV/dFh354IMPjCTz2WefddrG189ZKOpoWyxYsMDMmTPHp35s7BcheeSjI/X19YqLi7tqu9dee03Dhg3ThAkTVFBQoIsXw+c24y0tLaqsrFR2drZnXr9+/ZSdna2KiooOn1NRUeHVXpJycnI6bR+u6uvrJemq+8CFCxc0YsQIpaamas6cOTpy5IiN8gLu2LFjSk5O1qhRo/Sd73xHJ06c6LRtX9knWlpa9Oqrr+oHP/iB5yaUHemt+8SX1dTUqLa21ut9d7lcyszM7PR97873Tbiqr6+Xw+Ho9L5hl/nyOQsnZWVlio+P19ixY7V06VKdO3eu07a29ouwCB9VVVV68cUX9cMf/rDLdg8//LBeffVV7d69WwUFBfrDH/6g7373u5aq7Lm//e1vamtrazcSbEJCgmprazt8Tm1trU/tw5Hb7dby5cs1depUTZgwodN2Y8eO1W9/+1tt27ZNr776qtxut2677TadOnXKYrX+l5mZqeLiYu3YsUNFRUWqqanRN77xDTU2NnbYvi/sE5K0detWnT9/XgsXLuy0TW/dJ77q8nvry/vene+bcHTp0iWtWLFCDz30UJc3UfP1cxYuZs+erd///vcqLS3Vz3/+c5WXlys3N1dtbW0dtre1XwRkePXOPP300/r5z3/eZZtPPvnE6ySf//mf/9Hs2bN1//33a/HixV0+99FHH/X8PXHiRCUlJWnmzJmqrq7W6NGje1Y8giYvL08ff/zxVX9/zcrK8rp54W233aYbbrhBL7/8sn76058GusyAyc3N9fydkZGhzMxMjRgxQm+88YYWLVoUxMqC65VXXlFubm6Xt+/urfsErk1ra6seeOABGWNUVFTUZdve+jl78MEHPX9PnDhRGRkZGj16tMrKyjRz5syg1WU1fPz4xz/u8v9SJGnUqFGev0+fPq077rhDt912m/7t3/7N5/VlZmZK+uLISTiEj2HDhikiIkJ1dXVe8+vq6pSYmNjhcxITE31qH26WLVumt99+W3v27FFKSopPz42MjNTXv/51VVVVBai64IiNjdX111/f6evq7fuEJH322WfatWuX3nrrLZ+e11v3icvvbV1dnZKSkjzz6+rqNHny5A6f053vm3ByOXh89tlnevfdd32+dfzVPmfhatSoURo2bJiqqqo6DB+29gurP7sMHz5c48aN63KKioqS9MURj+nTp2vKlCnauHGj+vXzvdRDhw5JkteHMZRFRUVpypQpKi0t9cxzu90qLS31+r+3L8vKyvJqL0k7d+7stH24MMZo2bJl2rJli959912lp6f73EdbW5s++uijsHn/r9WFCxdUXV3d6evqrfvEl23cuFHx8fG6++67fXpeb90n0tPTlZiY6PW+NzQ0aN++fZ2+7935vgkXl4PHsWPHtGvXLg0dOtTnPq72OQtXp06d0rlz5zp9Xdb2C7+duupHp06dMmPGjDEzZ840p06dMmfOnPFMX24zduxYs2/fPmOMMVVVVWbNmjXmwIEDpqamxmzbts2MGjXKTJs2LVgvo1s2b95snE6nKS4uNn/961/No48+amJjY01tba0xxpjvfe975umnn/a0f//9903//v3NL3/5S/PJJ5+YlStXmsjISPPRRx8F6yX4xdKlS43L5TJlZWVe7//Fixc9bb66LVavXm1KSkpMdXW1qaysNA8++KCJjo42R44cCcZL8Jsf//jHpqyszNTU1Jj333/fZGdnm2HDhpmzZ88aY/rOPnFZW1ubSUtLMytWrGi3rDfvE42NjebDDz80H374oZFkfvWrX5kPP/zQcwXH2rVrTWxsrNm2bZs5fPiwmTNnjklPTzf/+7//6+ljxowZ5sUXX/Q8vtr3Tajqalu0tLSYb33rWyYlJcUcOnTI6/ujubnZ08dXt8XVPmehqqtt0djYaJ544glTUVFhampqzK5du8w///M/m+uuu85cunTJ00cw9ouQDB8bN240kjqcLqupqTGSzO7du40xxpw4ccJMmzbNxMXFGafTacaMGWOefPJJU19fH6RX0X0vvviiSUtLM1FRUeaWW24xe/fu9Sz75je/aRYsWODV/o033jDXX3+9iYqKMjfeeKN55513LFfsf529/xs3bvS0+eq2WL58uWe7JSQkmLvuusscPHjQfvF+Nn/+fJOUlGSioqLM1772NTN//nxTVVXlWd5X9onLSkpKjCRz9OjRdst68z6xe/fuDj8Tl1+v2+02zz77rElISDBOp9PMnDmz3TYaMWKEWblypde8rr5vQlVX2+Lyvw0dTZf/vTCm/ba42ucsVHW1LS5evGhmzZplhg8fbiIjI82IESPM4sWL24WIYOwXDmOM8d9xFAAAgK6FxaW2AACg9yB8AAAAqwgfAADAKsIHAACwivABAACsInwAAACrCB8AAMAqwgcAALCK8AEAAKwifAAAAKsIHwAAwCrCBwAAsOr/AZ9srmmEaO64AAAAAElFTkSuQmCC",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# BoxCox\n",
    "y = random_shuffle(np.random.rand(1000) * 10 + 5)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(BoxCox)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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KTFvvPGGad++vbnooouC9RUazUsvvWScTqdZvHix2bx5sxk9erRJTk42Ho/HGGPMTTfdZCZPnuxd/4MPPjAxMTHm0UcfNVu2bDHTpk0zsbGxZuPGjcH6CkFT376bMWOGWblypdm+fbspLi42w4YNM/Hx8WbTpk3B+gpBc+DAAe/fNEnmscceM+vXrzc7duwwxhgzefJkc9NNN3nX//rrr02TJk3Mvffea7Zs2WLy8vJMdHS0WbFiRbC+QtDUt+8ef/xxs2zZMvPVV1+ZjRs3mrvuustERUWZd955p177jbgQM2LECCPpmNd7773nXUeSWbRokTHGmEOHDpnLL7/cnHHGGSY2Nta0bdvWjBo1yvsHIdLUt/+M+fUy6ylTphiXy2WcTqcZMGCA2bp1a+MXH2Q//PCDueGGG0yzZs1MYmKiueWWW3zCX0lJiU9f7ty501x00UWmZcuWxul0mrPOOsvce++9pry8PEjfoHHNmzfPtGnTxsTFxZk+ffqYjz76yLvs4osvNiNGjPBZ/5VXXjF//OMfTVxcnDn77LPN22+/3cgVh4769N348eO967pcLnPllVeadevWBaHq4Dty2e/RryP9NWLECHPxxRcf85nu3bubuLg40759e5+/fZGkvn03e/Zs06FDBxMfH29atmxp+vfvb959991679dhjDGnNP4DAAAQBNwnBgAAWIkQAwAArESIAQAAViLEAAAAKxFiAACAlQgxAADASoQYAABgJUIMAACwEiEGAABYiRADAACsRIgBAABWIsQAAAAr/R83NoqpurcJkQAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# YeoJohnshon\n",
    "y = random_shuffle(np.random.randn(1000) * 10 + 5)\n",
    "y = np.random.beta(.5, .5, size=1000)\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(YeoJohnshon)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "y_tfm = preprocessor.transform(y)\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y)\n",
    "plt.hist(y, 50, label='ori',)\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAABZcAAABoCAYAAACNDM73AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAe6klEQVR4nO3deXBUVdrH8V9n62wkIRASQEIQwyKbrDEKCsWSIMMIqAiiA6igsssAiq8mAZFVKYYdpAasEQVxQBHZIkIQCfu+CAgJiQKCbCFs2c77h0NLSyA00h0Tvp+qruo+97n3PLf1geSpw7kWY4wRAAAAAAAAAAAOcCvsBAAAAAAAAAAARQ/NZQAAAAAAAACAw2guAwAAAAAAAAAcRnMZAAAAAAAAAOAwmssAAAAAAAAAAIfRXAYAAAAAAAAAOIzmMgAAAAAAAADAYTSXAQAAAAAAAAAOo7kMAAAAAAAAAHAYzWUAAAAnmTNnjiwWi1JTU21jTZs2VdOmTe/6XAkJCbJYLHZjERER6tat212f649SU1NlsVg0Z84c21i3bt3k7+/v9LmvsVgsSkhIcNl8AAAAAGguAwAA2OzevVtPP/20KlasKG9vb5UvX14tW7bUpEmTnDbnsWPHlJCQoB07djhtDkcsXbr0L9uk/SvnBgAAANyLPAo7AQAAgL+C9evXq1mzZgoPD1ePHj0UFham9PR0bdiwQf/617/Ut2/fuzLPypUr7T4fO3ZMw4YNU0REhB566KG7Msc1Bw4ckJubY2sJli5dqilTpjjUxK1YsaIuX74sT09PBzN0zK1yu3z5sjw8+NEWAAAAcCV+AgcAAJD03nvvKTAwUJs3b1ZQUJDdsZMnT961eby8vO7atQpitVqdev2cnBzl5eXJy8tL3t7eTp2rIIU9PwAAAHAvYlsMAAAASYcPH1aNGjVuaCxLUpkyZew+WywW9enTR3PnzlXVqlXl7e2t+vXra+3atQXOc/2ey2vWrFHDhg0lSd27d5fFYrlh7+L8rFu3Tg0bNpS3t7cqV66sGTNm5Bv3xz2Xs7OzNWzYMEVGRsrb21ulSpVS48aNlZiYKOm3fZKnTJliu8drL+n3fZXff/99TZgwQZUrV5bVatW+ffvy3XP5miNHjigmJkZ+fn4qV66chg8fLmOM7fiaNWtksVi0Zs0au/P+eM1b5XZt7I8rmrdv367WrVsrICBA/v7+at68uTZs2GAXc21f7O+//14DBw5USEiI/Pz81L59e506dSr//wAAAAAAJLFyGQAAQNJvWzskJydrz549qlmzZoHxSUlJmj9/vvr16yer1aqpU6cqNjZWmzZtuq3zJal69eoaPny44uLi1LNnTzVp0kSS9Mgjj9z0nN27d6tVq1YKCQlRQkKCcnJyFB8fr9DQ0ALnS0hI0KhRo/Tyyy+rUaNGysjI0JYtW7Rt2za1bNlSr7zyio4dO6bExET95z//yfcas2fP1pUrV9SzZ09ZrVYFBwcrLy8v39jc3FzFxsbq4Ycf1tixY7V8+XLFx8crJydHw4cPv41v6He3k9v19u7dqyZNmiggIEBDhgyRp6enZsyYoaZNmyopKUlRUVF28X379lXJkiUVHx+v1NRUTZgwQX369NH8+fMdyhMAAAC4l9BcBgAAkDRo0CC1bt1aDz30kBo1aqQmTZqoefPmatasWb57Ce/Zs0dbtmxR/fr1JUmdOnVS1apVFRcXp4ULF97WnKGhoWrdurXi4uIUHR2t559/vsBz4uLiZIzRd999p/DwcEnSU089pVq1ahV47tdff60nnnhCM2fOzPd4dHS0qlSposTExJvm8tNPP+nHH39USEiIbSw1NTXf2CtXrig2NlYTJ06UJPXq1Utt27bVmDFj1K9fP5UuXbrAnB3J7Xpvv/22srOztW7dOt1///2SpH/84x+qWrWqhgwZoqSkJLv4UqVKaeXKlbbV0Hl5eZo4caLOnz+vwMDA284TAAAAuJewLQYAAICkli1bKjk5WX//+9+1c+dOjR07VjExMSpfvrwWL158Q3x0dLStsSxJ4eHhevLJJ7VixQrl5uY6Jcfc3FytWLFC7dq1szWWpd9WQMfExBR4flBQkPbu3atDhw7dcQ5PPfWUXWO5IH369LG9v7adSFZWlr755ps7zqEgubm5Wrlypdq1a2drLEtS2bJl9dxzz2ndunXKyMiwO6dnz55222w0adJEubm5Onr0qNPyBAAAAIo6mssAAAD/07BhQy1cuFBnz57Vpk2bNHToUF24cEFPP/209u3bZxcbGRl5w/lVqlTRpUuXnLZX76lTp3T58uV8565atWqB5w8fPlznzp1TlSpVVKtWLQ0ePFi7du1yKIdKlSrddqybm5tdc1f67TuSbr7a+W44deqULl26lO93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",
      "text/plain": [
       "<Figure size 1600x50 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# QuantileTransformer\n",
    "y = - np.random.beta(1, .5, 10000) * 10\n",
    "splits = TimeSplitter()(y)\n",
    "preprocessor = Preprocessor(Quantile)\n",
    "preprocessor.fit(y[splits[0]])\n",
    "plt.hist(y, 50, label='ori',)\n",
    "y_tfm = preprocessor.transform(y)\n",
    "plt.legend(loc='best')\n",
    "plt.show()\n",
    "plt.hist(y_tfm, 50, label='tfm')\n",
    "plt.legend(loc='best')\n",
    "plt.show()\n",
    "test_close(preprocessor.inverse_transform(y_tfm), y, 1e-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#|export\n",
    "def ReLabeler(cm):\n",
    "    r\"\"\"Changes the labels in a dataset based on a dictionary (class mapping) \n",
    "        Args:\n",
    "            cm = class mapping dictionary\n",
    "    \"\"\"\n",
    "    def _relabel(y):\n",
    "        obj = len(set([len(listify(v)) for v in cm.values()])) > 1\n",
    "        keys = cm.keys()\n",
    "        if obj: \n",
    "            new_cm = {k:v for k,v in zip(keys, [listify(v) for v in cm.values()])}\n",
    "            return np.array([new_cm[yi] if yi in keys else listify(yi) for yi in y], dtype=object).reshape(*y.shape)\n",
    "        else: \n",
    "            new_cm = {k:v for k,v in zip(keys, [listify(v) for v in cm.values()])}\n",
    "            return np.array([new_cm[yi] if yi in keys else listify(yi) for yi in y]).reshape(*y.shape)\n",
    "    return _relabel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(array(['d', 'd', 'a', 'd', 'b', 'e', 'a', 'd', 'b', 'c', 'b', 'e', 'b',\n",
       "        'b', 'a', 'e', 'd', 'e', 'c', 'e'], dtype='<U1'),\n",
       " array(['z', 'z', 'x', 'z', 'x', 'z', 'x', 'z', 'x', 'y', 'x', 'z', 'x',\n",
       "        'x', 'x', 'z', 'z', 'z', 'y', 'z'], dtype='<U1'))"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vals = {0:'a', 1:'b', 2:'c', 3:'d', 4:'e'}\n",
    "y = np.array([vals[i] for i in np.random.randint(0, 5, 20)])\n",
    "labeler = ReLabeler(dict(a='x', b='x', c='y', d='z', e='z'))\n",
    "y_new = labeler(y)\n",
    "test_eq(y.shape, y_new.shape)\n",
    "y, y_new"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/javascript": "IPython.notebook.save_checkpoint();",
      "text/plain": [
       "<IPython.core.display.Javascript object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/Users/nacho/notebooks/tsai/nbs/009_data.preprocessing.ipynb couldn't be saved automatically. You should save it manually 👋\n",
      "Correct notebook to script conversion! 😃\n",
      "Saturday 17/06/23 17:17:25 CEST\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "\n",
       "                <audio  controls=\"controls\" autoplay=\"autoplay\">\n",
       "                    <source src=\"data:audio/wav;base64,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\" type=\"audio/wav\" />\n",
       "                    Your browser does not support the audio element.\n",
       "                </audio>\n",
       "              "
      ],
      "text/plain": [
       "<IPython.lib.display.Audio object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#|eval: false\n",
    "#|hide\n",
    "from tsai.export import get_nb_name; nb_name = get_nb_name(locals())\n",
    "from tsai.imports import create_scripts; create_scripts(nb_name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
